summaryrefslogtreecommitdiff
path: root/includes
diff options
context:
space:
mode:
authorPierre Schmitz <pierre@archlinux.de>2008-08-15 01:29:47 +0200
committerPierre Schmitz <pierre@archlinux.de>2008-08-15 01:29:47 +0200
commit370e83bb0dfd0c70de268c93bf07ad5ee0897192 (patch)
tree491674f4c242e4d6ba0d04eafa305174c35a3391 /includes
parentf4debf0f12d0524d2b2427c55ea3f16b680fad97 (diff)
Update auf 1.13.0
Diffstat (limited to 'includes')
-rw-r--r--includes/AjaxDispatcher.php23
-rw-r--r--includes/AjaxFunctions.php33
-rw-r--r--includes/AjaxResponse.php10
-rw-r--r--includes/Article.php708
-rw-r--r--includes/AuthPlugin.php4
-rw-r--r--includes/AutoLoader.php669
-rw-r--r--includes/Autopromote.php24
-rw-r--r--includes/BagOStuff.php135
-rw-r--r--includes/Block.php138
-rw-r--r--includes/CacheDependency.php46
-rw-r--r--includes/Category.php261
-rw-r--r--includes/CategoryPage.php125
-rw-r--r--includes/Categoryfinder.php2
-rw-r--r--includes/ChangesFeed.php129
-rw-r--r--includes/ChangesList.php471
-rw-r--r--includes/Credits.php4
-rw-r--r--includes/DatabaseFunctions.php4
-rw-r--r--includes/DefaultSettings.php1107
-rw-r--r--includes/Defines.php83
-rw-r--r--includes/DifferenceEngine.php367
-rw-r--r--includes/DjVuImage.php55
-rw-r--r--includes/DoubleRedirectJob.php166
-rw-r--r--includes/EditPage.php590
-rw-r--r--includes/EmaillingJob.php5
-rw-r--r--includes/EnotifNotifyJob.php24
-rw-r--r--includes/Exception.php91
-rw-r--r--includes/Exif.php15
-rw-r--r--includes/Export.php149
-rw-r--r--includes/ExternalEdit.php3
-rw-r--r--includes/ExternalStore.php34
-rw-r--r--includes/ExternalStoreDB.php20
-rw-r--r--includes/ExternalStoreHttp.php5
-rw-r--r--includes/FakeTitle.php2
-rw-r--r--includes/Feed.php2
-rw-r--r--includes/FeedUtils.php152
-rw-r--r--includes/FileDeleteForm.php142
-rw-r--r--includes/FileRevertForm.php91
-rw-r--r--includes/FileStore.php80
-rw-r--r--includes/FormOptions.php202
-rw-r--r--includes/GlobalFunctions.php632
-rw-r--r--includes/HTMLCacheUpdate.php33
-rw-r--r--includes/HTMLFileCache.php17
-rw-r--r--includes/HistoryBlob.php8
-rw-r--r--includes/Hooks.php7
-rw-r--r--includes/HttpFunctions.php3
-rw-r--r--includes/IP.php34
-rw-r--r--includes/ImageFunctions.php13
-rw-r--r--includes/ImageGallery.php29
-rw-r--r--includes/ImagePage.php647
-rw-r--r--includes/ImageQueryPage.php5
-rw-r--r--includes/JobQueue.php57
-rw-r--r--includes/Licenses.php5
-rw-r--r--includes/LinkBatch.php64
-rw-r--r--includes/LinkCache.php121
-rw-r--r--includes/LinkFilter.php1
-rw-r--r--includes/Linker.php464
-rw-r--r--includes/LinksUpdate.php227
-rw-r--r--includes/LogEventsList.php742
-rw-r--r--includes/LogPage.php112
-rw-r--r--includes/MacBinary.php4
-rw-r--r--includes/MagicWord.php68
-rw-r--r--includes/Math.php9
-rw-r--r--includes/MediaTransformOutput.php51
-rw-r--r--includes/MemcachedSessions.php4
-rw-r--r--includes/MessageCache.php726
-rw-r--r--includes/Metadata.php4
-rw-r--r--includes/MimeMagic.php51
-rw-r--r--includes/Namespace.php55
-rw-r--r--includes/NamespaceCompat.php9
-rw-r--r--includes/ObjectCache.php11
-rw-r--r--includes/OutputHandler.php16
-rw-r--r--includes/OutputPage.php451
-rw-r--r--includes/PageHistory.php258
-rw-r--r--includes/PageQueryPage.php9
-rw-r--r--includes/Pager.php260
-rw-r--r--includes/PatrolLog.php6
-rw-r--r--includes/PrefixSearch.php85
-rw-r--r--includes/Profiler.php243
-rw-r--r--includes/ProfilerSimple.php17
-rw-r--r--includes/ProfilerSimpleText.php35
-rw-r--r--includes/ProfilerSimpleUDP.php11
-rw-r--r--includes/ProfilerStub.php46
-rw-r--r--includes/ProtectionForm.php221
-rw-r--r--includes/ProxyTools.php48
-rw-r--r--includes/QueryPage.php56
-rw-r--r--includes/RawPage.php20
-rw-r--r--includes/RecentChange.php125
-rw-r--r--includes/RefreshLinksJob.php5
-rw-r--r--includes/Revision.php283
-rw-r--r--includes/Sanitizer.php35
-rw-r--r--includes/SearchEngine.php831
-rw-r--r--includes/SearchMySQL.php71
-rw-r--r--includes/SearchMySQL4.php60
-rw-r--r--includes/SearchOracle.php13
-rw-r--r--includes/SearchPostgres.php36
-rw-r--r--includes/SearchUpdate.php10
-rw-r--r--includes/Setup.php78
-rw-r--r--includes/SiteConfiguration.php10
-rw-r--r--includes/SiteStats.php19
-rw-r--r--includes/Skin.php229
-rw-r--r--includes/SkinTemplate.php167
-rw-r--r--includes/SpecialPage.php170
-rw-r--r--includes/SquidUpdate.php19
-rw-r--r--includes/Status.php194
-rw-r--r--includes/StreamFile.php8
-rw-r--r--includes/StringUtils.php49
-rw-r--r--includes/StubObject.php98
-rw-r--r--includes/Title.php619
-rw-r--r--includes/User.php905
-rw-r--r--includes/UserArray.php62
-rw-r--r--includes/UserMailer.php85
-rw-r--r--includes/UserRightsProxy.php45
-rw-r--r--includes/WatchedItem.php13
-rw-r--r--includes/WatchlistEditor.php105
-rw-r--r--includes/WebRequest.php150
-rw-r--r--includes/WebResponse.php2
-rw-r--r--includes/WebStart.php39
-rw-r--r--includes/Wiki.php281
-rw-r--r--includes/WikiError.php19
-rw-r--r--includes/Xml.php279
-rw-r--r--includes/XmlFunctions.php6
-rw-r--r--includes/XmlTypeCheck.php22
-rw-r--r--includes/ZhClient.php17
-rw-r--r--includes/ZhConversion.php4946
-rw-r--r--includes/api/ApiBase.php134
-rw-r--r--includes/api/ApiBlock.php10
-rw-r--r--includes/api/ApiDelete.php117
-rw-r--r--includes/api/ApiEditPage.php299
-rw-r--r--includes/api/ApiEmailUser.php114
-rw-r--r--includes/api/ApiExpandTemplates.php38
-rw-r--r--includes/api/ApiFeedWatchlist.php33
-rw-r--r--includes/api/ApiFormatBase.php51
-rw-r--r--includes/api/ApiFormatDbg.php5
-rw-r--r--includes/api/ApiFormatJson.php7
-rw-r--r--includes/api/ApiFormatJson_json.php19
-rw-r--r--includes/api/ApiFormatPhp.php5
-rw-r--r--includes/api/ApiFormatTxt.php5
-rw-r--r--includes/api/ApiFormatWddx.php5
-rw-r--r--includes/api/ApiFormatXml.php34
-rw-r--r--includes/api/ApiFormatYaml.php5
-rw-r--r--includes/api/ApiFormatYaml_spyc.php235
-rw-r--r--includes/api/ApiHelp.php7
-rw-r--r--includes/api/ApiLogin.php73
-rw-r--r--includes/api/ApiLogout.php10
-rw-r--r--includes/api/ApiMain.php150
-rw-r--r--includes/api/ApiMove.php50
-rw-r--r--includes/api/ApiOpenSearch.php22
-rw-r--r--includes/api/ApiPageSet.php200
-rw-r--r--includes/api/ApiParamInfo.php11
-rw-r--r--includes/api/ApiParse.php83
-rw-r--r--includes/api/ApiProtect.php15
-rw-r--r--includes/api/ApiQuery.php67
-rw-r--r--includes/api/ApiQueryAllCategories.php63
-rw-r--r--includes/api/ApiQueryAllLinks.php49
-rw-r--r--includes/api/ApiQueryAllUsers.php81
-rw-r--r--includes/api/ApiQueryAllimages.php205
-rw-r--r--includes/api/ApiQueryAllmessages.php25
-rw-r--r--includes/api/ApiQueryAllpages.php48
-rw-r--r--includes/api/ApiQueryBacklinks.php371
-rw-r--r--includes/api/ApiQueryBase.php208
-rw-r--r--includes/api/ApiQueryBlocks.php59
-rw-r--r--includes/api/ApiQueryCategories.php73
-rw-r--r--includes/api/ApiQueryCategoryInfo.php91
-rw-r--r--includes/api/ApiQueryCategoryMembers.php75
-rw-r--r--includes/api/ApiQueryDeletedrevs.php30
-rw-r--r--includes/api/ApiQueryExtLinksUsage.php65
-rw-r--r--includes/api/ApiQueryExternalLinks.php51
-rw-r--r--includes/api/ApiQueryImageInfo.php162
-rw-r--r--includes/api/ApiQueryImages.php70
-rw-r--r--includes/api/ApiQueryInfo.php344
-rw-r--r--includes/api/ApiQueryLangLinks.php61
-rw-r--r--includes/api/ApiQueryLinks.php78
-rw-r--r--includes/api/ApiQueryLogEvents.php57
-rw-r--r--includes/api/ApiQueryRandom.php16
-rw-r--r--includes/api/ApiQueryRecentChanges.php120
-rw-r--r--includes/api/ApiQueryRevisions.php183
-rw-r--r--includes/api/ApiQuerySearch.php19
-rw-r--r--includes/api/ApiQuerySiteinfo.php304
-rw-r--r--includes/api/ApiQueryUserContributions.php69
-rw-r--r--includes/api/ApiQueryUserInfo.php18
-rw-r--r--includes/api/ApiQueryUsers.php47
-rw-r--r--includes/api/ApiQueryWatchlist.php29
-rw-r--r--includes/api/ApiResult.php36
-rw-r--r--includes/api/ApiRollback.php13
-rw-r--r--includes/api/ApiUnblock.php11
-rw-r--r--includes/api/ApiUndelete.php17
-rw-r--r--includes/cbt/CBTCompiler.php35
-rw-r--r--includes/cbt/CBTProcessor.php81
-rw-r--r--includes/cbt/README44
-rw-r--r--includes/db/Database.php2699
-rw-r--r--includes/db/DatabaseMssql.php1029
-rw-r--r--includes/db/DatabaseOracle.php720
-rw-r--r--includes/db/DatabasePostgres.php1394
-rw-r--r--includes/db/DatabaseSqlite.php405
-rw-r--r--includes/db/LBFactory.php261
-rw-r--r--includes/db/LBFactory_Multi.php233
-rw-r--r--includes/db/LoadBalancer.php918
-rw-r--r--includes/db/LoadMonitor.php121
-rw-r--r--includes/filerepo/ArchivedFile.php72
-rw-r--r--includes/filerepo/FSRepo.php44
-rw-r--r--includes/filerepo/File.php221
-rw-r--r--includes/filerepo/FileRepo.php174
-rw-r--r--includes/filerepo/FileRepoStatus.php151
-rw-r--r--includes/filerepo/ForeignAPIFile.php101
-rw-r--r--includes/filerepo/ForeignAPIRepo.php110
-rw-r--r--includes/filerepo/ForeignDBFile.php33
-rw-r--r--includes/filerepo/ForeignDBRepo.php13
-rw-r--r--includes/filerepo/ForeignDBViaLBRepo.php39
-rw-r--r--includes/filerepo/Image.php74
-rw-r--r--includes/filerepo/LocalFile.php533
-rw-r--r--includes/filerepo/LocalRepo.php103
-rw-r--r--includes/filerepo/NullRepo.php8
-rw-r--r--includes/filerepo/OldLocalFile.php192
-rw-r--r--includes/filerepo/README14
-rw-r--r--includes/filerepo/RepoGroup.php74
-rw-r--r--includes/filerepo/UnregisteredLocalFile.php9
-rw-r--r--includes/media/BMP.php8
-rw-r--r--includes/media/Bitmap.php22
-rw-r--r--includes/media/DjVu.php20
-rw-r--r--includes/media/Generic.php56
-rw-r--r--includes/media/SVG.php18
-rw-r--r--includes/memcached-client.php79
-rw-r--r--includes/mime.info15
-rw-r--r--includes/mime.types9
-rw-r--r--includes/normal/CleanUpTest.php3
-rw-r--r--includes/normal/Makefile9
-rw-r--r--includes/normal/RandomTest.php4
-rw-r--r--includes/normal/Utf8Case.php2078
-rw-r--r--includes/normal/Utf8CaseGenerate.php112
-rw-r--r--includes/normal/Utf8Test.php4
-rw-r--r--includes/normal/UtfNormal.php56
-rw-r--r--includes/normal/UtfNormalBench.php4
-rw-r--r--includes/normal/UtfNormalData.inc2
-rw-r--r--includes/normal/UtfNormalDataK.inc2
-rw-r--r--includes/normal/UtfNormalDefines.php53
-rw-r--r--includes/normal/UtfNormalGenerate.php4
-rw-r--r--includes/normal/UtfNormalTest.php4
-rw-r--r--includes/normal/UtfNormalUtil.php5
-rw-r--r--includes/parser/CoreParserFunctions.php385
-rw-r--r--includes/parser/DateFormatter.php283
-rw-r--r--includes/parser/Parser.php5002
-rw-r--r--includes/parser/ParserCache.php116
-rw-r--r--includes/parser/ParserOptions.php149
-rw-r--r--includes/parser/ParserOutput.php206
-rw-r--r--includes/parser/Parser_DiffTest.php87
-rw-r--r--includes/parser/Parser_OldPP.php4944
-rw-r--r--includes/parser/Preprocessor.php163
-rw-r--r--includes/parser/Preprocessor_DOM.php1421
-rw-r--r--includes/parser/Preprocessor_Hash.php1539
-rw-r--r--includes/proxy_check.php2
-rw-r--r--includes/specials/SpecialAllmessages.php217
-rw-r--r--includes/specials/SpecialAllpages.php404
-rw-r--r--includes/specials/SpecialAncientpages.php60
-rw-r--r--includes/specials/SpecialBlankpage.php6
-rw-r--r--includes/specials/SpecialBlockip.php494
-rw-r--r--includes/specials/SpecialBlockme.php37
-rw-r--r--includes/specials/SpecialBooksources.php110
-rw-r--r--includes/specials/SpecialBrokenRedirects.php93
-rw-r--r--includes/specials/SpecialCategories.php112
-rw-r--r--includes/specials/SpecialConfirmemail.php139
-rw-r--r--includes/specials/SpecialContributions.php470
-rw-r--r--includes/specials/SpecialDeadendpages.php62
-rw-r--r--includes/specials/SpecialDisambiguations.php108
-rw-r--r--includes/specials/SpecialDoubleRedirects.php103
-rw-r--r--includes/specials/SpecialEmailuser.php286
-rw-r--r--includes/specials/SpecialExport.php284
-rw-r--r--includes/specials/SpecialFewestrevisions.php74
-rw-r--r--includes/specials/SpecialFileDuplicateSearch.php135
-rw-r--r--includes/specials/SpecialFilepath.php53
-rw-r--r--includes/specials/SpecialImagelist.php161
-rw-r--r--includes/specials/SpecialImport.php1154
-rw-r--r--includes/specials/SpecialIpblocklist.php427
-rw-r--r--includes/specials/SpecialListgrouprights.php112
-rw-r--r--includes/specials/SpecialListredirects.php58
-rw-r--r--includes/specials/SpecialListusers.php235
-rw-r--r--includes/specials/SpecialLockdb.php131
-rw-r--r--includes/specials/SpecialLog.php65
-rw-r--r--includes/specials/SpecialLonelypages.php58
-rw-r--r--includes/specials/SpecialLongpages.php31
-rw-r--r--includes/specials/SpecialMIMEsearch.php138
-rw-r--r--includes/specials/SpecialMergeHistory.php448
-rw-r--r--includes/specials/SpecialMostcategories.php58
-rw-r--r--includes/specials/SpecialMostimages.php54
-rw-r--r--includes/specials/SpecialMostlinked.php95
-rw-r--r--includes/specials/SpecialMostlinkedcategories.php78
-rw-r--r--includes/specials/SpecialMostlinkedtemplates.php132
-rw-r--r--includes/specials/SpecialMostrevisions.php64
-rw-r--r--includes/specials/SpecialMovepage.php452
-rw-r--r--includes/specials/SpecialNewimages.php211
-rw-r--r--includes/specials/SpecialNewpages.php437
-rw-r--r--includes/specials/SpecialPopularpages.php67
-rw-r--r--includes/specials/SpecialPreferences.php1126
-rw-r--r--includes/specials/SpecialPrefixindex.php152
-rw-r--r--includes/specials/SpecialProtectedpages.php309
-rw-r--r--includes/specials/SpecialProtectedtitles.php216
-rw-r--r--includes/specials/SpecialRandompage.php100
-rw-r--r--includes/specials/SpecialRandomredirect.php19
-rw-r--r--includes/specials/SpecialRecentchanges.php662
-rw-r--r--includes/specials/SpecialRecentchangeslinked.php178
-rw-r--r--includes/specials/SpecialResetpass.php167
-rw-r--r--includes/specials/SpecialRevisiondelete.php1474
-rw-r--r--includes/specials/SpecialSearch.php651
-rw-r--r--includes/specials/SpecialShortpages.php98
-rw-r--r--includes/specials/SpecialSpecialpages.php82
-rw-r--r--includes/specials/SpecialStatistics.php93
-rw-r--r--includes/specials/SpecialUncategorizedcategories.php30
-rw-r--r--includes/specials/SpecialUncategorizedimages.php48
-rw-r--r--includes/specials/SpecialUncategorizedpages.php55
-rw-r--r--includes/specials/SpecialUncategorizedtemplates.php33
-rw-r--r--includes/specials/SpecialUndelete.php1276
-rw-r--r--includes/specials/SpecialUnlockdb.php107
-rw-r--r--includes/specials/SpecialUnusedcategories.php46
-rw-r--r--includes/specials/SpecialUnusedimages.php60
-rw-r--r--includes/specials/SpecialUnusedtemplates.php54
-rw-r--r--includes/specials/SpecialUnwatchedpages.php68
-rw-r--r--includes/specials/SpecialUpload.php1755
-rw-r--r--includes/specials/SpecialUploadMogile.php135
-rw-r--r--includes/specials/SpecialUserlogin.php929
-rw-r--r--includes/specials/SpecialUserlogout.php23
-rw-r--r--includes/specials/SpecialUserrights.php589
-rw-r--r--includes/specials/SpecialVersion.php391
-rw-r--r--includes/specials/SpecialWantedcategories.php90
-rw-r--r--includes/specials/SpecialWantedpages.php131
-rw-r--r--includes/specials/SpecialWatchlist.php383
-rw-r--r--includes/specials/SpecialWhatlinkshere.php408
-rw-r--r--includes/specials/SpecialWithoutinterwiki.php88
-rw-r--r--includes/templates/NoLocalSettings.php5
-rw-r--r--includes/templates/Userlogin.php68
-rw-r--r--includes/tidy.conf6
-rw-r--r--includes/zhtable/Makefile118
-rw-r--r--includes/zhtable/README21
-rw-r--r--includes/zhtable/simp2trad.manual96
-rw-r--r--includes/zhtable/simp2trad_noconvert.manual4
-rw-r--r--includes/zhtable/simp2trad_supp_set.manual2
-rw-r--r--includes/zhtable/simpphrases.manual389
-rw-r--r--includes/zhtable/simpphrases_exclude.manual0
-rw-r--r--includes/zhtable/toCN.manual11
-rw-r--r--includes/zhtable/toHK.manual10
-rw-r--r--includes/zhtable/toSG.manual6
-rw-r--r--includes/zhtable/toSimp.manual21
-rw-r--r--includes/zhtable/toTW.manual34
-rw-r--r--includes/zhtable/toTrad.manual42
-rw-r--r--includes/zhtable/trad2simp.manual11
-rw-r--r--includes/zhtable/trad2simp_noconvert.manual4
-rw-r--r--includes/zhtable/trad2simp_supp_set.manual1
-rw-r--r--includes/zhtable/trad2simp_supp_unset.manual0
-rw-r--r--includes/zhtable/tradphrases.manual1522
-rw-r--r--includes/zhtable/tradphrases_exclude.manual161
348 files changed, 67745 insertions, 8718 deletions
diff --git a/includes/AjaxDispatcher.php b/includes/AjaxDispatcher.php
index 7b85ed20..c489cf1c 100644
--- a/includes/AjaxDispatcher.php
+++ b/includes/AjaxDispatcher.php
@@ -1,5 +1,9 @@
<?php
/**
+ * @defgroup Ajax Ajax
+ *
+ * @file
+ * @ingroup Ajax
* Handle ajax requests and send them to the proper handler.
*/
@@ -11,7 +15,7 @@ require_once( 'AjaxFunctions.php' );
/**
* Object-Oriented Ajax functions.
- * @addtogroup Ajax
+ * @ingroup Ajax
*/
class AjaxDispatcher {
/** The way the request was made, either a 'get' or a 'post' */
@@ -58,6 +62,7 @@ class AjaxDispatcher {
break;
default:
+ wfProfileOut( __METHOD__ );
return;
# Or we could throw an exception:
#throw new MWException( __METHOD__ . ' called without any data (mode empty).' );
@@ -81,9 +86,13 @@ class AjaxDispatcher {
wfProfileIn( __METHOD__ );
if (! in_array( $this->func_name, $wgAjaxExportList ) ) {
+ wfDebug( __METHOD__ . ' Bad Request for unknown function ' . $this->func_name . "\n" );
+
wfHttpError( 400, 'Bad Request',
"unknown function " . (string) $this->func_name );
} else {
+ wfDebug( __METHOD__ . ' dispatching ' . $this->func_name . "\n" );
+
if ( strpos( $this->func_name, '::' ) !== false ) {
$func = explode( '::', $this->func_name, 2 );
} else {
@@ -93,6 +102,10 @@ class AjaxDispatcher {
$result = call_user_func_array($func, $this->args);
if ( $result === false || $result === NULL ) {
+ wfDebug( __METHOD__ . ' ERROR while dispatching '
+ . $this->func_name . "(" . var_export( $this->args, true ) . "): "
+ . "no data returned\n" );
+
wfHttpError( 500, 'Internal Error',
"{$this->func_name} returned no data" );
}
@@ -103,9 +116,15 @@ class AjaxDispatcher {
$result->sendHeaders();
$result->printText();
+
+ wfDebug( __METHOD__ . ' dispatch complete for ' . $this->func_name . "\n" );
}
} catch (Exception $e) {
+ wfDebug( __METHOD__ . ' ERROR while dispatching '
+ . $this->func_name . "(" . var_export( $this->args, true ) . "): "
+ . get_class($e) . ": " . $e->getMessage() . "\n" );
+
if (!headers_sent()) {
wfHttpError( 500, 'Internal Error',
$e->getMessage() );
@@ -119,5 +138,3 @@ class AjaxDispatcher {
$wgOut = null;
}
}
-
-
diff --git a/includes/AjaxFunctions.php b/includes/AjaxFunctions.php
index ffd3168a..9daca9e5 100644
--- a/includes/AjaxFunctions.php
+++ b/includes/AjaxFunctions.php
@@ -1,8 +1,7 @@
<?php
-
-/**
- * @package MediaWiki
- * @addtogroup Ajax
+/**
+ * @file
+ * @ingroup Ajax
*/
if( !defined( 'MEDIAWIKI' ) ) {
@@ -56,7 +55,7 @@ function js_unescape($source, $iconv_to = 'UTF-8') {
/**
* Function coverts number of utf char into that character.
- * Function taken from: http://sk2.php.net/manual/en/function.utf8-encode.php#49336
+ * Function taken from: http://www.php.net/manual/en/function.utf8-encode.php#49336
*
* @param $num Integer
* @return utf8char
@@ -76,7 +75,7 @@ function code2utf($num){
define( 'AJAX_SEARCH_VERSION', 2 ); //AJAX search cache version
function wfSajaxSearch( $term ) {
- global $wgContLang, $wgOut, $wgUser, $wgCapitalLinks, $wgMemc;
+ global $wgContLang, $wgUser, $wgCapitalLinks, $wgMemc;
$limit = 16;
$sk = $wgUser->getSkin();
$output = '';
@@ -84,7 +83,7 @@ function wfSajaxSearch( $term ) {
$term = trim( $term );
$term = $wgContLang->checkTitleEncoding( $wgContLang->recodeInput( js_unescape( $term ) ) );
if ( $wgCapitalLinks )
- $term = $wgContLang->ucfirst( $term );
+ $term = $wgContLang->ucfirst( $term );
$term_title = Title::newFromText( $term );
$memckey = $term_title ? wfMemcKey( 'ajaxsearch', md5( $term_title->getFullText() ) ) : wfMemcKey( 'ajaxsearch', md5( $term ) );
@@ -97,12 +96,12 @@ function wfSajaxSearch( $term ) {
$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] );
@@ -128,9 +127,10 @@ function wfSajaxSearch( $term ) {
$valid = (bool) $term_title;
$term_url = urlencode( $term );
- $term_diplay = htmlspecialchars( $valid ? $term_title->getFullText() : $term );
+ $term_normalized = $valid ? $term_title->getFullText() : $term;
+ $term_display = htmlspecialchars( $term );
$subtitlemsg = ( $valid ? 'searchsubtitle' : 'searchsubtitleinvalid' );
- $subtitle = wfMsgWikiHtml( $subtitlemsg, $term_diplay );
+ $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')
@@ -138,15 +138,15 @@ function wfSajaxSearch( $term ) {
if( $canSearch ) {
$html .= '<ul><li>'
. $sk->makeKnownLink( $wgContLang->specialPage( 'Search' ),
- wfMsgHtml( 'searchcontaining', $term_diplay ),
+ wfMsgHtml( 'searchcontaining', $term_display ),
"search={$term_url}&fulltext=Search" )
. '</li><li>' . $sk->makeKnownLink( $wgContLang->specialPage( 'Search' ),
- wfMsgHtml( 'searchnamed', $term_diplay ) ,
+ wfMsgHtml( 'searchnamed', $term_display ) ,
"search={$term_url}&go=Go" )
. "</li></ul>";
}
if( $r ) {
- $html .= "<h2>" . wfMsgHtml( 'articletitles', $term_diplay ) . "</h2>"
+ $html .= "<h2>" . wfMsgHtml( 'articletitles', $term_display ) . "</h2>"
. '<ul>' .$r .'</ul>' . $more;
}
@@ -161,7 +161,7 @@ function wfSajaxSearch( $term ) {
* Called for AJAX watch/unwatch requests.
* @param $pagename Prefixed title string for page to watch/unwatch
* @param $watch String 'w' to watch, 'u' to unwatch
- * @return String '<w#>' or '<u#>' on successful watch or unwatch,
+ * @return String '<w#>' or '<u#>' on successful watch or unwatch,
* respectively, followed by an HTML message to display in the alert box; or
* '<err#>' on error
*/
@@ -169,7 +169,7 @@ function wfAjaxWatch($pagename = "", $watch = "") {
if(wfReadOnly()) {
// redirect to action=(un)watch, which will display the database lock
// message
- return '<err#>';
+ return '<err#>';
}
if('w' !== $watch && 'u' !== $watch) {
@@ -206,4 +206,3 @@ function wfAjaxWatch($pagename = "", $watch = "") {
return '<u#>'.wfMsgExt( 'removedwatchtext', array( 'parse' ), $title->getPrefixedText() );
}
}
-
diff --git a/includes/AjaxResponse.php b/includes/AjaxResponse.php
index 8fa08539..c79e928b 100644
--- a/includes/AjaxResponse.php
+++ b/includes/AjaxResponse.php
@@ -1,11 +1,16 @@
<?php
+/**
+ * @file
+ * @ingroup Ajax
+ */
+
if( !defined( 'MEDIAWIKI' ) ) {
die( 1 );
}
/**
* @todo document
- * @addtogroup Ajax
+ * @ingroup Ajax
*/
class AjaxResponse {
@@ -99,7 +104,7 @@ class AjaxResponse {
if ( $this->mCacheDuration ) {
- # If squid caches are configured, tell them to cache the response,
+ # If squid caches are configured, tell them to cache the response,
# and tell the client to always check with the squid. Otherwise,
# tell the client to use a cached copy, without a way to purge it.
@@ -220,4 +225,3 @@ class AjaxResponse {
return true;
}
}
-
diff --git a/includes/Article.php b/includes/Article.php
index 0544db7d..4d8277bb 100644
--- a/includes/Article.php
+++ b/includes/Article.php
@@ -1,6 +1,7 @@
<?php
/**
* File for articles
+ * @file
*/
/**
@@ -34,6 +35,8 @@ class Article {
var $mTouched; //!<
var $mUser; //!<
var $mUserText; //!<
+ var $mRedirectTarget; //!<
+ var $mIsRedirect;
/**@}}*/
/**
@@ -57,12 +60,69 @@ class Article {
}
/**
+ * If this page is a redirect, get its target
+ *
+ * The target will be fetched from the redirect table if possible.
+ * If this page doesn't have an entry there, call insertRedirect()
+ * @return mixed Title object, or null if this page is not a redirect
+ */
+ public function getRedirectTarget() {
+ if(!$this->mTitle || !$this->mTitle->isRedirect())
+ return null;
+ 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__
+ );
+ $row = $dbr->fetchObject($res);
+ if($row)
+ 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();
+ }
+
+ /**
+ * Insert an entry for this page into the redirect table.
+ *
+ * Don't call this function directly unless you know what you're doing.
+ * @return Title object
+ */
+ public function insertRedirect() {
+ $retval = Title::newFromRedirect($this->getContent());
+ if(!$retval)
+ return null;
+ $dbw = wfGetDB(DB_MASTER);
+ $dbw->replace('redirect', array('rd_from'), array(
+ 'rd_from' => $this->getID(),
+ 'rd_namespace' => $retval->getNamespace(),
+ 'rd_title' => $retval->getDBKey()
+ ), __METHOD__);
+ return $retval;
+ }
+
+ /**
+ * Get the Title object this page redirects to
+ *
* @return mixed false, Title of in-wiki target, or string with URL
*/
- function followRedirect() {
+ public function followRedirect() {
$text = $this->getContent();
+ return self::followRedirectText( $text );
+ }
+
+ /**
+ * Get the Title object this text redirects to
+ *
+ * @return mixed false, Title of in-wiki target, or string with URL
+ */
+ public function followRedirectText( $text ) {
$rt = Title::newFromRedirect( $text );
-
# process if title object is valid and not special:userlogout
if( $rt ) {
if( $rt->getInterwiki() != '' ) {
@@ -92,7 +152,6 @@ class Article {
return $rt;
}
}
-
// No or invalid redirect
return false;
}
@@ -114,6 +173,7 @@ class Article {
$this->mCurID = $this->mUser = $this->mCounter = -1; # Not loaded
$this->mRedirectedFrom = null; # Title object if set
+ $this->mRedirectTarget = null; # Title object if set
$this->mUserText =
$this->mTimestamp = $this->mComment = '';
$this->mGoodAdjustment = $this->mTotalAdjustment = 0;
@@ -138,7 +198,7 @@ class Article {
* @return Return the text of this revision
*/
function getContent() {
- global $wgUser, $wgOut;
+ global $wgUser, $wgOut, $wgMessageCache;
wfProfileIn( __METHOD__ );
@@ -147,12 +207,13 @@ class Article {
$wgOut->setRobotpolicy( 'noindex,nofollow' );
if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
+ $wgMessageCache->loadAllMessages();
$ret = wfMsgWeirdKey ( $this->mTitle->getText() ) ;
} else {
$ret = wfMsg( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon' );
}
- return "<div class='noarticletext'>$ret</div>";
+ return "<div class='noarticletext'>\n$ret\n</div>";
} else {
$this->loadContent();
wfProfileOut( __METHOD__ );
@@ -298,13 +359,13 @@ class Article {
*/
function loadPageData( $data = 'fromdb' ) {
if ( $data === 'fromdb' ) {
- $dbr = $this->getDB();
+ $dbr = wfGetDB( DB_MASTER );
$data = $this->pageDataFromId( $dbr, $this->getId() );
}
- $lc =& LinkCache::singleton();
+ $lc = LinkCache::singleton();
if ( $data ) {
- $lc->addGoodLinkObj( $data->page_id, $this->mTitle );
+ $lc->addGoodLinkObj( $data->page_id, $this->mTitle, $data->page_len, $data->page_is_redirect );
$this->mTitle->mArticleID = $data->page_id;
@@ -336,15 +397,13 @@ class Article {
return $this->mContent;
}
- $dbr = $this->getDB();
+ $dbr = wfGetDB( DB_MASTER );
# Pre-fill content with error message so that if something
# fails we'll have something telling us what we intended.
$t = $this->mTitle->getPrefixedText();
- if( $oldid ) {
- $t .= ',oldid='.$oldid;
- }
- $this->mContent = wfMsg( 'missingarticle', $t ) ;
+ $d = $oldid ? wfMsgExt( 'missingarticle-rev', array( 'escape' ), $oldid ) : '';
+ $this->mContent = wfMsg( 'missing-article', $t, $d ) ;
if( $oldid ) {
$revision = Revision::newFromId( $oldid );
@@ -377,15 +436,14 @@ class Article {
// 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->userCan( Revision::DELETED_TEXT ) ? $revision->getRawText() : "";
- //$this->mContent = $revision->getText();
+ $this->mContent = $revision->revText(); // Loads if user is allowed
$this->mUser = $revision->getUser();
$this->mUserText = $revision->getUserText();
$this->mComment = $revision->getComment();
$this->mTimestamp = wfTimestamp( TS_MW, $revision->getTimestamp() );
- $this->mRevIdFetched = $revision->getID();
+ $this->mRevIdFetched = $revision->getId();
$this->mContentLoaded = true;
$this->mRevision =& $revision;
@@ -407,8 +465,10 @@ class Article {
* Get the database which should be used for reads
*
* @return Database
+ * @deprecated - just call wfGetDB( DB_MASTER ) instead
*/
function getDB() {
+ wfDeprecated( __METHOD__ );
return wfGetDB( DB_MASTER );
}
@@ -490,6 +550,10 @@ class Article {
*/
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() );
} else {
@@ -526,14 +590,14 @@ class Article {
$id = $this->getID();
if ( 0 == $id ) return;
- $this->mLastRevision = Revision::loadFromPageId( $this->getDB(), $id );
+ $this->mLastRevision = Revision::loadFromPageId( wfGetDB( DB_MASTER ), $id );
if( !is_null( $this->mLastRevision ) ) {
$this->mUser = $this->mLastRevision->getUser();
$this->mUserText = $this->mLastRevision->getUserText();
$this->mTimestamp = $this->mLastRevision->getTimestamp();
$this->mComment = $this->mLastRevision->getComment();
$this->mMinorEdit = $this->mLastRevision->isMinor();
- $this->mRevIdFetched = $this->mLastRevision->getID();
+ $this->mRevIdFetched = $this->mLastRevision->getId();
}
}
@@ -571,7 +635,6 @@ class Article {
}
/**
- * @todo Document, fixme $offset never used.
* @param $limit Integer: default 0.
* @param $offset Integer: default 0.
*/
@@ -593,6 +656,8 @@ class Article {
ORDER BY timestamp DESC";
if ($limit > 0) { $sql .= ' LIMIT '.$limit; }
+ if ($offset > 0) { $sql .= ' OFFSET '.$offset; }
+
$sql .= ' '. $this->getSelectOptions();
$res = $dbr->query($sql, __METHOD__);
@@ -609,7 +674,7 @@ class Article {
* This is the default action of the script: just view the page of
* the given title.
*/
- function view() {
+ function view() {
global $wgUser, $wgOut, $wgRequest, $wgContLang;
global $wgEnableParserCache, $wgStylePath, $wgParser;
global $wgUseTrackbacks, $wgNamespaceRobotPolicies, $wgArticleRobotPolicies;
@@ -618,7 +683,7 @@ class Article {
wfProfileIn( __METHOD__ );
- $parserCache =& ParserCache::singleton();
+ $parserCache = ParserCache::singleton();
$ns = $this->mTitle->getNamespace(); # shortcut
# Get variables from query string
@@ -689,12 +754,7 @@ class Article {
}
# Should the parser cache be used?
- $pcache = $wgEnableParserCache
- && intval( $wgUser->getOption( 'stubthreshold' ) ) == 0
- && $this->exists()
- && empty( $oldid )
- && !$this->mTitle->isCssOrJsPage()
- && !$this->mTitle->isCssJsSubpage();
+ $pcache = $this->useParserCache( $oldid );
wfDebug( 'Article::view using parser cache: ' . ($pcache ? 'yes' : 'no' ) . "\n" );
if ( $wgUser->getOption( 'stubthreshold' ) ) {
wfIncrStats( 'pcache_miss_stub' );
@@ -705,7 +765,6 @@ class Article {
// This is an internally redirected page view.
// We'll need a backlink to the source page for navigation.
if ( wfRunHooks( 'ArticleViewRedirect', array( &$this ) ) ) {
- $sk = $wgUser->getSkin();
$redir = $sk->makeKnownLinkObj( $this->mRedirectedFrom, '', 'redirect=no' );
$s = wfMsg( 'redirectedfrom', $redir );
$wgOut->setSubtitle( $s );
@@ -722,7 +781,6 @@ class Article {
// If it was reported from a trusted site, supply a backlink.
global $wgRedirectSources;
if( $wgRedirectSources && preg_match( $wgRedirectSources, $rdfrom ) ) {
- $sk = $wgUser->getSkin();
$redir = $sk->makeExternalLink( $rdfrom, $rdfrom );
$s = wfMsg( 'redirectedfrom', $redir );
$wgOut->setSubtitle( $s );
@@ -740,16 +798,17 @@ class Article {
$outputDone = true;
}
}
+ # Fetch content and check for errors
if ( !$outputDone ) {
$text = $this->getContent();
if ( $text === false ) {
# Failed to load, replace text with error message
$t = $this->mTitle->getPrefixedText();
if( $oldid ) {
- $t .= ',oldid='.$oldid;
- $text = wfMsg( 'missingarticle', $t );
+ $d = wfMsgExt( 'missingarticle-rev', array( 'escape' ), $oldid );
+ $text = wfMsg( 'missing-article', $t, $d );
} else {
- $text = wfMsg( 'noarticletext', $t );
+ $text = wfMsg( 'noarticletext' );
}
}
@@ -757,11 +816,11 @@ class Article {
if ( !$this->mTitle->userCanRead() ) {
$wgOut->loginToUse();
$wgOut->output();
+ wfProfileOut( __METHOD__ );
exit;
}
# We're looking at an old revision
-
if ( !empty( $oldid ) ) {
$wgOut->setRobotpolicy( 'noindex,nofollow' );
if( is_null( $this->mRevision ) ) {
@@ -772,6 +831,7 @@ class Article {
if( !$this->mRevision->userCan( Revision::DELETED_TEXT ) ) {
$wgOut->addWikiMsg( 'rev-deleted-text-permission' );
$wgOut->setPageTitle( $this->mTitle->getPrefixedText() );
+ wfProfileOut( __METHOD__ );
return;
} else {
$wgOut->addWikiMsg( 'rev-deleted-text-view' );
@@ -779,16 +839,13 @@ class Article {
}
}
}
-
}
- }
- if( !$outputDone ) {
- $wgOut->setRevisionId( $this->getRevIdFetched() );
+ $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' ) );
-
// 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
@@ -798,22 +855,9 @@ class Article {
$wgOut->addHtml( htmlspecialchars( $this->mContent ) );
$wgOut->addHtml( "\n</pre>\n" );
}
-
- }
-
- elseif ( $rt = Title::newFromRedirect( $text ) ) {
- # Display redirect
- $imageDir = $wgContLang->isRTL() ? 'rtl' : 'ltr';
- $imageUrl = $wgStylePath.'/common/images/redirect' . $imageDir . '.png';
- # Don't overwrite the subtitle if this was an old revision
- if( !$wasRedirected && $this->isCurrent() ) {
- $wgOut->setSubtitle( wfMsgHtml( 'redirectpagesub' ) );
- }
- $link = $sk->makeLinkObj( $rt, $rt->getFullText() );
-
- $wgOut->addHTML( '<img src="'.$imageUrl.'" alt="#REDIRECT " />' .
- '<span class="redirectText">'.$link.'</span>' );
-
+ } else if ( $rt = Title::newFromRedirect( $text ) ) {
+ # Don't append the subtitle if this was an old revision
+ $this->viewRedirect( $rt, !$wasRedirected && $this->isCurrent() );
$parseout = $wgParser->parse($text, $this->mTitle, ParserOptions::newFromUser($wgUser));
$wgOut->addParserOutputNoText( $parseout );
} else if ( $pcache ) {
@@ -840,7 +884,6 @@ class Article {
if( !$this->isCurrent() ) {
$wgOut->parserOptions()->setEditSection( $oldEditSectionSetting );
}
-
}
}
/* title may have been set from the cache */
@@ -850,8 +893,7 @@ class Article {
}
# check if we're displaying a [[User talk:x.x.x.x]] anonymous talk page
- if( $ns == NS_USER_TALK &&
- User::isIP( $this->mTitle->getText() ) ) {
+ if( $ns == NS_USER_TALK && IP::isValid( $this->mTitle->getText() ) ) {
$wgOut->addWikiMsg('anontalkpagetext');
}
@@ -875,6 +917,41 @@ class Article {
$this->viewUpdates();
wfProfileOut( __METHOD__ );
}
+
+ /*
+ * Should the parser cache be used?
+ */
+ protected function useParserCache( $oldid ) {
+ global $wgUser, $wgEnableParserCache;
+
+ return $wgEnableParserCache
+ && intval( $wgUser->getOption( 'stubthreshold' ) ) == 0
+ && $this->exists()
+ && empty( $oldid )
+ && !$this->mTitle->isCssOrJsPage()
+ && !$this->mTitle->isCssJsSubpage();
+ }
+
+ protected 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 )
+ $link = $sk->makeKnownLinkObj( $target, htmlspecialchars( $target->getFullText() ) );
+ else
+ $link = $sk->makeLinkObj( $target, htmlspecialchars( $target->getFullText() ) );
+
+ $wgOut->addHTML( '<img src="'.$imageUrl.'" alt="#REDIRECT " />' .
+ '<span class="redirectText">'.$link.'</span>' );
+
+ }
function addTrackbacks() {
global $wgOut, $wgUser;
@@ -897,6 +974,7 @@ class Article {
. $o->tb_id . "&token=" . urlencode( $wgUser->editToken() ) );
$rmvtxt = wfMsg( 'trackbackremove', htmlspecialchars( $delurl ) );
}
+ $tbtext .= "\n";
$tbtext .= wfMsg(strlen($o->tb_ex) ? 'trackbackexcerpt' : 'trackback',
$o->tb_title,
$o->tb_url,
@@ -1067,7 +1145,6 @@ class Article {
$result = $dbw->affectedRows() != 0;
if ($result) {
- // FIXME: Should the result from updateRedirectOn() be returned instead?
$this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect );
}
@@ -1112,6 +1189,8 @@ class Article {
$dbw->delete( 'redirect', $where, __METHOD__);
}
+ if( $this->getTitle()->getNamespace() == NS_IMAGE )
+ RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $this->getTitle() );
wfProfileOut( __METHOD__ );
return ( $dbw->affectedRows() != 0 );
}
@@ -1252,10 +1331,10 @@ class Article {
}
}
- $extraq = ''; // Give extensions a chance to modify URL query on update
- wfRunHooks( 'ArticleUpdateBeforeRedirect', array( $this, &$sectionanchor, &$extraq ) );
+ $extraQuery = ''; // Give extensions a chance to modify URL query on update
+ wfRunHooks( 'ArticleUpdateBeforeRedirect', array( $this, &$sectionanchor, &$extraQuery ) );
- $this->doRedirect( $this->isRedirect( $text ), $sectionanchor, $extraq );
+ $this->doRedirect( $this->isRedirect( $text ), $sectionanchor, $extraQuery );
}
return $good;
}
@@ -1291,11 +1370,12 @@ class Article {
* 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
*
* @return bool success
*/
- function doEdit( $text, $summary, $flags = 0 ) {
- global $wgUser, $wgDBtransactions;
+ function doEdit( $text, $summary, $flags = 0, $baseRevId = false ) {
+ global $wgUser, $wgDBtransactions, $wgUseAutomaticEditSummaries;
wfProfileIn( __METHOD__ );
$good = true;
@@ -1325,9 +1405,10 @@ class Article {
$oldtext = $this->getContent();
$oldsize = strlen( $oldtext );
- # Provide autosummaries if one is not provided.
- if ($flags & EDIT_AUTOSUMMARY && $summary == '')
+ # Provide autosummaries if one is not provided and autosummaries are enabled.
+ if( $wgUseAutomaticEditSummaries && $flags & EDIT_AUTOSUMMARY && $summary == '' ) {
$summary = $this->getAutosummary( $oldtext, $text, $flags );
+ }
$editInfo = $this->prepareTextForEdit( $text );
$text = $editInfo->pst;
@@ -1346,7 +1427,7 @@ class Article {
$lastRevision = 0;
$revisionId = 0;
-
+
$changed = ( strcmp( $text, $oldtext ) != 0 );
if ( $changed ) {
@@ -1368,7 +1449,8 @@ class Article {
'page' => $this->getId(),
'comment' => $summary,
'minor_edit' => $isminor,
- 'text' => $text
+ 'text' => $text,
+ 'parent_id' => $lastRevision
) );
$dbw->begin();
@@ -1382,6 +1464,8 @@ class Article {
$good = false;
$dbw->rollback();
} else {
+ wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, $baseRevId ) );
+
# Update recentchanges
if( !( $flags & EDIT_SUPPRESS_RC ) ) {
$rcid = RecentChange::notifyEdit( $now, $this->mTitle, $isminor, $wgUser, $summary,
@@ -1414,7 +1498,7 @@ class Article {
# Invalidate cache of this article and all pages using this article
# as a template. Partly deferred.
Article::onArticleEdit( $this->mTitle );
-
+
# Update links tables, site stats, etc.
$this->editUpdates( $text, $summary, $isminor, $now, $revisionId, $changed );
}
@@ -1446,6 +1530,8 @@ class Article {
# Update the page record with revision data
$this->updateRevisionOn( $dbw, $revision, 0 );
+
+ wfRunHooks( 'NewRevisionFromEditComplete', array($this, $revision, false) );
if( !( $flags & EDIT_SUPPRESS_RC ) ) {
$rcid = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $wgUser, $summary, $bot,
@@ -1495,15 +1581,16 @@ class Article {
*
* @param boolean $noRedir Add redirect=no
* @param string $sectionAnchor section to redirect to, including "#"
+ * @param string $extraQuery, extra query params
*/
- function doRedirect( $noRedir = false, $sectionAnchor = '', $extraq = '' ) {
+ function doRedirect( $noRedir = false, $sectionAnchor = '', $extraQuery = '' ) {
global $wgOut;
if ( $noRedir ) {
$query = 'redirect=no';
- if( $extraq )
+ if( $extraQuery )
$query .= "&$query";
} else {
- $query = $extraq;
+ $query = $extraQuery;
}
$wgOut->redirect( $this->mTitle->getFullURL( $query ) . $sectionAnchor );
}
@@ -1518,8 +1605,8 @@ class Article {
# Check patrol config options
if ( !($wgUseNPPatrol || $wgUseRCPatrol)) {
- $wgOut->errorPage( 'rcpatroldisabled', 'rcpatroldisabledtext' );
- return;
+ $wgOut->showErrorPage( 'rcpatroldisabled', 'rcpatroldisabledtext' );
+ return;
}
# If we haven't been given an rc_id value, we can't do anything
@@ -1527,18 +1614,18 @@ class Article {
$rc = $rcid ? RecentChange::newFromId($rcid) : null;
if ( is_null ( $rc ) )
{
- $wgOut->errorPage( 'markedaspatrollederror', 'markedaspatrollederrortext' );
+ $wgOut->showErrorPage( 'markedaspatrollederror', 'markedaspatrollederrortext' );
return;
}
- if ( !$wgUseRCPatrol && $rc->mAttribs['rc_type'] != RC_NEW) {
+ 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->errorPage( 'rcpatroldisabled', 'rcpatroldisabledtext' );
+ $wgOut->showErrorPage( 'rcpatroldisabled', 'rcpatroldisabledtext' );
return;
}
-
+
# Check permissions
$permission_errors = $this->mTitle->getUserPermissionsErrors( 'patrol', $wgUser );
@@ -1554,7 +1641,7 @@ class Article {
}
#It would be nice to see where the user had actually come from, but for now just guess
- $returnto = $rc->mAttribs['rc_type'] == RC_NEW ? 'Newpages' : 'Recentchanges';
+ $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
@@ -1575,10 +1662,14 @@ class Article {
}
}
- # Mark the edit as patrolled
- RecentChange::markPatrolled( $rcid );
- PatrolLog::record( $rcid );
- wfRunHooks( 'MarkPatrolledComplete', array( &$rcid, &$wgUser, false ) );
+ # 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 ) );
+ }
# Inform the user
$wgOut->setPageTitle( wfMsg( 'markedaspatrolled' ) );
@@ -1724,8 +1815,8 @@ class Article {
$updated = Article::flattenRestrictions( $limit );
$changed = ( $current != $updated );
- $changed = $changed || ($this->mTitle->areRestrictionsCascading() != $cascade);
- $changed = $changed || ($this->mTitle->mRestrictionsExpiry != $expiry);
+ $changed = $changed || ($updated && $this->mTitle->areRestrictionsCascading() != $cascade);
+ $changed = $changed || ($updated && $this->mTitle->mRestrictionsExpiry != $expiry);
$protect = ( $updated != '' );
# If nothing's changed, do nothing
@@ -1751,12 +1842,16 @@ class Article {
}
$comment = $wgContLang->ucfirst( wfMsgForContent( $comment_type, $this->mTitle->getPrefixedText() ) );
- foreach( $limit as $action => $restrictions ) {
- # Check if the group level required to edit also can protect pages
- # Otherwise, people who cannot normally protect can "protect" pages via transclusion
- $cascade = ( $cascade && isset($wgGroupPermissions[$restrictions]['protect']) && $wgGroupPermissions[$restrictions]['protect'] );
+ # 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').']';
@@ -1770,8 +1865,7 @@ class Article {
$comment .= "$expiry_description";
if ( $cascade )
$comment .= "$cascade_description";
-
- $rowsAffected = false;
+
# Update restrictions table
foreach( $limit as $action => $restrictions ) {
if ($restrictions != '' ) {
@@ -1779,18 +1873,11 @@ class Article {
array( 'pr_page' => $id, 'pr_type' => $action
, 'pr_level' => $restrictions, 'pr_cascade' => $cascade ? 1 : 0
, 'pr_expiry' => $encodedExpiry ), __METHOD__ );
- if($dbw->affectedRows() != 0)
- $rowsAffected = true;
} else {
$dbw->delete( 'page_restrictions', array( 'pr_page' => $id,
'pr_type' => $action ), __METHOD__ );
- if($dbw->affectedRows() != 0)
- $rowsAffected = true;
}
}
- if(!$rowsAffected)
- // No change
- return true;
# Insert a null revision
$nullRevision = Revision::newNullRevision( $dbw, $id, $comment, true );
@@ -1806,15 +1893,15 @@ class Article {
'page_id' => $id
), 'Article::protect'
);
+
+ wfRunHooks( 'NewRevisionFromEditComplete', array($this, $nullRevision, false) );
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" ) );
+ $log->addEntry( $modified ? 'modify' : 'protect', $this->mTitle,
+ trim( $reason . " [$updated]$cascade_description$expiry_description" ) );
} else {
$log->addEntry( 'unprotect', $this->mTitle, $reason );
}
@@ -1845,7 +1932,7 @@ class Article {
}
return implode( ':', $bits );
}
-
+
/**
* Auto-generates a deletion reason
* @param bool &$hasHistory Whether the page has a history
@@ -1907,7 +1994,7 @@ class Article {
else
$reason = wfMsgForContent('excontent', '$1');
}
-
+
// Replace newlines with spaces to prevent uglyness
$contents = preg_replace("/[\n\r]/", ' ', $contents);
// Calculate the maximum amount of chars to get
@@ -1930,18 +2017,20 @@ class Article {
$confirm = $wgRequest->wasPosted() &&
$wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) );
-
+
$this->DeleteReasonList = $wgRequest->getText( 'wpDeleteReasonList', 'other' );
$this->DeleteReason = $wgRequest->getText( 'wpReason' );
-
+
$reason = $this->DeleteReasonList;
-
+
if ( $reason != 'other' && $this->DeleteReason != '') {
// Entry from drop down menu + additional comment
$reason .= ': ' . $this->DeleteReason;
} elseif ( $reason == 'other' ) {
$reason = $this->DeleteReason;
}
+ # Flag to hide all contents of the archived revisions
+ $suppress = $wgRequest->getVal( 'wpSuppress' ) && $wgUser->isAllowed('suppressrevision');
# This code desperately needs to be totally rewritten
@@ -1950,7 +2039,7 @@ class Article {
$wgOut->readOnlyPage();
return;
}
-
+
# Check permissions
$permission_errors = $this->mTitle->getUserPermissionsErrors( 'delete', $wgUser );
@@ -1980,7 +2069,7 @@ class Article {
}
if( $confirm ) {
- $this->doDelete( $reason );
+ $this->doDelete( $reason, $suppress );
if( $wgRequest->getCheck( 'wpWatch' ) ) {
$this->doWatch();
} elseif( $this->mTitle->userIsWatching() ) {
@@ -2003,10 +2092,10 @@ class Article {
array( 'delete-warning-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ) );
}
}
-
- return $this->confirmDelete( '', $reason );
+
+ return $this->confirmDelete( $reason );
}
-
+
/**
* @return bool whether or not the page surpasses $wgDeleteRevisionsLimit revisions
*/
@@ -2018,7 +2107,7 @@ class Article {
}
return false;
}
-
+
/**
* @return int approximate revision count
*/
@@ -2079,10 +2168,9 @@ class Article {
/**
* Output deletion confirmation dialog
- * @param $par string FIXME: do we need this parameter? One Call from Article::delete with '' only.
* @param $reason string Prefilled reason
*/
- function confirmDelete( $par, $reason ) {
+ function confirmDelete( $reason ) {
global $wgOut, $wgUser, $wgContLang;
$align = $wgContLang->isRtl() ? 'left' : 'right';
@@ -2092,9 +2180,17 @@ class Article {
$wgOut->setRobotpolicy( 'noindex,nofollow' );
$wgOut->addWikiMsg( 'confirmdeletetext' );
- $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->mTitle->getLocalURL( 'action=delete' . $par ), 'id' => 'deleteconfirm' ) ) .
- Xml::openElement( 'fieldset' ) .
- Xml::element( 'legend', array(), wfMsg( 'delete-legend' ) ) .
+ 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>";
+ } else {
+ $suppress = '';
+ }
+
+ $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' ) .
"<tr id=\"wpDeleteReasonListRow\">
<td align='$align'>" .
@@ -2102,7 +2198,7 @@ class Article {
"</td>
<td>" .
Xml::listDropDown( 'wpDeleteReasonList',
- wfMsgForContent( 'deletereason-dropdown' ),
+ wfMsgForContent( 'deletereason-dropdown' ),
wfMsgForContent( 'deletereasonotherlist' ), '', 'wpReasonDropDown', 1 ) .
"</td>
</tr>
@@ -2120,6 +2216,7 @@ class Article {
Xml::checkLabel( wfMsg( 'watchthis' ), 'wpWatch', 'wpWatch', $wgUser->getBoolOption( 'watchdeletion' ) || $this->mTitle->userIsWatching(), array( 'tabindex' => '3' ) ) .
"</td>
</tr>
+ $suppress
<tr>
<td></td>
<td>" .
@@ -2131,6 +2228,12 @@ class Article {
Xml::hidden( 'wpEditToken', $wgUser->editToken() ) .
Xml::closeElement( 'form' );
+ 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 );
}
@@ -2140,25 +2243,24 @@ class Article {
* Show relevant lines from the deletion log
*/
function showLogExtract( $out ) {
- $out->addHtml( '<h2>' . htmlspecialchars( LogPage::logName( 'delete' ) ) . '</h2>' );
- $logViewer = new LogViewer(
- new LogReader(
- new FauxRequest(
- array( 'page' => $this->mTitle->getPrefixedText(),
- 'type' => 'delete' ) ) ) );
- $logViewer->showList( $out );
+ $out->addHtml( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) );
+ LogEventsList::showLogExtract( $out, 'delete', $this->mTitle->getPrefixedText() );
}
/**
* Perform a deletion and output success or failure messages
*/
- function doDelete( $reason ) {
+ function doDelete( $reason, $suppress = false ) {
global $wgOut, $wgUser;
wfDebug( __METHOD__."\n" );
+
+ $id = $this->getId();
+
+ $error = '';
- if (wfRunHooks('ArticleDelete', array(&$this, &$wgUser, &$reason))) {
- if ( $this->doDeleteArticle( $reason ) ) {
+ if (wfRunHooks('ArticleDelete', array(&$this, &$wgUser, &$reason, &$error))) {
+ if ( $this->doDeleteArticle( $reason, $suppress ) ) {
$deleted = $this->mTitle->getPrefixedText();
$wgOut->setPagetitle( wfMsg( 'actioncomplete' ) );
@@ -2168,9 +2270,12 @@ class Article {
$wgOut->addWikiMsg( 'deletedtext', $deleted, $loglink );
$wgOut->returnToMain( false );
- wfRunHooks('ArticleDeleteComplete', array(&$this, &$wgUser, $reason));
+ wfRunHooks('ArticleDeleteComplete', array(&$this, &$wgUser, $reason, $id));
} else {
- $wgOut->showFatalError( wfMsg( 'cannotdelete' ) );
+ if ($error = '')
+ $wgOut->showFatalError( wfMsg( 'cannotdelete' ) );
+ else
+ $wgOut->showFatalError( $error );
}
}
}
@@ -2180,7 +2285,7 @@ class Article {
* Deletes the article with database consistency, writes logs, purges caches
* Returns success
*/
- function doDeleteArticle( $reason ) {
+ function doDeleteArticle( $reason, $suppress = false ) {
global $wgUseSquid, $wgDeferredUpdateList;
global $wgUseTrackbacks;
@@ -2198,6 +2303,19 @@ class Article {
$u = new SiteStatsUpdate( 0, 1, -(int)$this->isCountable( $this->getContent() ), -1 );
array_push( $wgDeferredUpdateList, $u );
+ // Bitfields to further suppress the content
+ if ( $suppress ) {
+ $bitfield = 0;
+ // This should be 15...
+ $bitfield |= Revision::DELETED_TEXT;
+ $bitfield |= Revision::DELETED_COMMENT;
+ $bitfield |= Revision::DELETED_USER;
+ $bitfield |= Revision::DELETED_RESTRICTED;
+ } else {
+ $bitfield = 'rev_deleted';
+ }
+
+ $dbw->begin();
// For now, shunt the revision data into the archive table.
// Text is *not* removed from the text table; bulk storage
// is left intact to avoid breaking block-compression or
@@ -2221,8 +2339,9 @@ class Article {
'ar_text_id' => 'rev_text_id',
'ar_text' => '\'\'', // Be explicit to appease
'ar_flags' => '\'\'', // MySQL's "strict mode"...
- 'ar_len' => 'rev_len',
+ 'ar_len' => 'rev_len',
'ar_page_id' => 'page_id',
+ 'ar_deleted' => $bitfield
), array(
'page_id' => $id,
'page_id = rev_page'
@@ -2232,12 +2351,25 @@ 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
+ if( !$ok ) {
+ $dbw->rollback();
+ return false;
+ }
# If using cascading deletes, we can skip some explicit deletes
if ( !$dbw->cascadingDeletes() ) {
-
$dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ );
if ($wgUseTrackbacks)
@@ -2257,19 +2389,26 @@ class Article {
if ( !$dbw->cleanupTriggers() ) {
# Clean up recentchanges entries...
- $dbw->delete( 'recentchanges', array( 'rc_namespace' => $ns, 'rc_title' => $t ), __METHOD__ );
+ $dbw->delete( 'recentchanges',
+ array( 'rc_namespace' => $ns, 'rc_title' => $t, 'rc_type != '.RC_LOG ),
+ __METHOD__ );
}
+ $dbw->commit();
# Clear caches
Article::onArticleDelete( $this->mTitle );
- # Log the deletion
- $log = new LogPage( 'delete' );
- $log->addEntry( 'delete', $this->mTitle, $reason );
-
# Clear the cached article id so the interface doesn't act like we exist
$this->mTitle->resetArticleID( 0 );
$this->mTitle->mArticleID = 0;
+
+ # Log the deletion, if the page was suppressed, log it at Oversight instead
+ $logtype = $suppress ? 'suppress' : 'delete';
+ $log = new LogPage( $logtype );
+
+ # Make sure logging got through
+ $log->addEntry( 'delete', $this->mTitle, $reason, array() );
+
return true;
}
@@ -2280,15 +2419,15 @@ 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 $fromP - Name of the user whose edits to rollback.
* @param string $summary - Custom summary. Set to default summary if empty.
* @param string $token - Rollback token.
* @param bool $bot - If true, mark all reverted edits as bot.
- *
+ *
* @param array $resultDetails contains result-specific array of additional values
* 'alreadyrolled' : 'current' (rev)
* success : 'summary' (str), 'current' (rev), 'target' (rev)
- *
+ *
* @return array of errors, each error formatted as
* array(messagekey, param1, param2, ...).
* On success, the array is empty. This array can also be passed to
@@ -2310,10 +2449,10 @@ class Article {
# If there were errors, bail out now
if(!empty($errors))
return $errors;
-
+
return $this->commitRollback($fromP, $summary, $bot, $resultDetails);
}
-
+
/**
* Backend implementation of doRollback(), please refer there for parameter
* and return value documentation
@@ -2322,9 +2461,9 @@ class Article {
* rollback to the DB Therefore, you should only call this function direct-
* ly if you want to use custom permissions checks. If you don't, use
* doRollback() instead.
- */
+ */
public function commitRollback($fromP, $summary, $bot, &$resultDetails) {
- global $wgUseRCPatrol, $wgUser;
+ global $wgUseRCPatrol, $wgUser, $wgLang;
$dbw = wfGetDB( DB_MASTER );
if( wfReadOnly() ) {
@@ -2352,7 +2491,7 @@ class Article {
$user = intval( $current->getUser() );
$user_text = $dbw->addQuotes( $current->getUserText() );
$s = $dbw->selectRow( 'revision',
- array( 'rev_id', 'rev_timestamp' ),
+ array( 'rev_id', 'rev_timestamp', 'rev_deleted' ),
array( 'rev_page' => $current->getPage(),
"rev_user <> {$user} OR rev_user_text <> {$user_text}"
), __METHOD__,
@@ -2362,8 +2501,11 @@ class Article {
if( $s === false ) {
# No one else ever edited this page
return array(array('cantrollback'));
+ } else if( $s->rev_deleted & REVISION::DELETED_TEXT || $s->rev_deleted & REVISION::DELETED_USER ) {
+ # Only admins can see this text
+ return array(array('notvisiblerev'));
}
-
+
$set = array();
if ( $bot && $wgUser->isAllowed('markbotedits') ) {
# Mark all reverted edits as bot
@@ -2386,15 +2528,17 @@ class Article {
# Generate the edit summary if necessary
$target = Revision::newFromId( $s->rev_id );
- if( empty( $summary ) )
- {
- global $wgLang;
- $summary = wfMsgForContent( 'revertpage',
- $target->getUserText(), $from,
- $s->rev_id, $wgLang->timeanddate(wfTimestamp(TS_MW, $s->rev_timestamp), true),
- $current->getId(), $wgLang->timeanddate($current->getTimestamp())
- );
+ 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 );
# Save
$flags = EDIT_UPDATE;
@@ -2404,7 +2548,7 @@ class Article {
if( $bot && ($wgUser->isAllowed('markbotedits') || $wgUser->isAllowed('bot')) )
$flags |= EDIT_FORCE_BOT;
- $this->doEdit( $target->getText(), $summary, $flags );
+ $this->doEdit( $target->getText(), $summary, $flags, $target->getId() );
wfRunHooks( 'ArticleRollbackComplete', array( $this, $wgUser, $target ) );
@@ -2439,6 +2583,19 @@ class Article {
$wgOut->rateLimited();
return;
}
+ if( isset( $result[0][0] ) && ( $result[0][0] == 'alreadyrolled' || $result[0][0] == 'cantrollback' ) ){
+ $wgOut->setPageTitle( wfMsg( 'rollbackfailed' ) );
+ $errArray = $result[0];
+ $errMsg = array_shift( $errArray );
+ $wgOut->addWikiMsgArray( $errMsg, $errArray );
+ if( isset( $details['current'] ) ){
+ $current = $details['current'];
+ if( $current->getComment() != '' ) {
+ $wgOut->addWikiMsgArray( 'editcomment', array( $wgUser->getSkin()->formatComment( $current->getComment() ) ), array( 'replaceafter' ) );
+ }
+ }
+ return;
+ }
# Display permissions errors before read-only message -- there's no
# point in misleading the user into thinking the inability to rollback
# is only temporary.
@@ -2469,6 +2626,11 @@ class Article {
. $wgUser->getSkin()->userToolLinks( $target->getUser(), $target->getUserText() );
$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 );
+ $de->showDiff( '', '' );
+ }
}
@@ -2477,11 +2639,12 @@ class Article {
* @private
*/
function viewUpdates() {
- global $wgDeferredUpdateList;
+ global $wgDeferredUpdateList, $wgUser;
if ( 0 != $this->getID() ) {
+ # Don't update page view counters on views from bot users (bug 14044)
global $wgDisableCounters;
- if( !$wgDisableCounters ) {
+ if( !$wgDisableCounters && !$wgUser->isAllowed( 'bot' ) ) {
Article::incViewCount( $this->getID() );
$u = new SiteStatsUpdate( 1, 0, 0 );
array_push( $wgDeferredUpdateList, $u );
@@ -2489,7 +2652,6 @@ class Article {
}
# Update newtalk / watchlist notification status
- global $wgUser;
$wgUser->clearNotification( $this->mTitle );
}
@@ -2546,7 +2708,7 @@ class Article {
# Save it to the parser cache
if ( $wgEnableParserCache ) {
- $parserCache =& ParserCache::singleton();
+ $parserCache = ParserCache::singleton();
$parserCache->save( $editInfo->output, $this, $wgUser );
}
@@ -2646,7 +2808,7 @@ class Article {
$sk = $wgUser->getSkin();
$lnk = $current
? wfMsg( 'currentrevisionlink' )
- : $lnk = $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'currentrevisionlink' ) );
+ : $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'currentrevisionlink' ) );
$curdiff = $current
? wfMsg( 'diff' )
: $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'diff' ), 'diff=cur&oldid='.$oldid );
@@ -2664,16 +2826,38 @@ class Article {
? wfMsg( 'diff' )
: $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'diff' ), 'diff=next&oldid='.$oldid );
- $userlinks = $sk->userLink( $revision->getUser(), $revision->getUserText() )
- . $sk->userToolLinks( $revision->getUser(), $revision->getUserText() );
+ $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');
+ } else if( !$revision->userCan( Revision::DELETED_RESTRICTED ) ) {
+ // If revision was hidden from sysops
+ $cdel = wfMsgHtml('rev-delundel');
+ } else {
+ $cdel = $sk->makeKnownLinkObj( $revdel,
+ wfMsgHtml('rev-delundel'),
+ 'target=' . urlencode( $this->mTitle->getPrefixedDbkey() ) .
+ '&oldid=' . urlencode( $oldid ) );
+ // Bolden oversighted content
+ if( $revision->isDeleted( Revision::DELETED_RESTRICTED ) )
+ $cdel = "<strong>$cdel</strong>";
+ }
+ $cdel = "(<small>$cdel</small>) ";
+ }
+ # Show user links if allowed to see them. Normally they
+ # are hidden regardless, but since we can already see the text here...
+ $userlinks = $sk->revUserTools( $revision, false );
$m = wfMsg( 'revision-info-current' );
$infomsg = $current && !wfEmptyMsg( 'revision-info-current', $m ) && $m != '-'
? 'revision-info-current'
: 'revision-info';
-
+
$r = "\n\t\t\t\t<div id=\"mw-{$infomsg}\">" . wfMsg( $infomsg, $td, $userlinks ) . "</div>\n" .
- "\n\t\t\t\t<div id=\"mw-revision-nav\">" . wfMsg( 'revision-nav', $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff ) . "</div>\n\t\t\t";
+
+ "\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";
$wgOut->setSubtitle( $r );
}
@@ -2731,9 +2915,9 @@ class Article {
$printable = $wgRequest->getVal( 'printable' );
$page = $wgRequest->getVal( 'page' );
- //check for non-standard user language; this covers uselang,
+ //check for non-standard user language; this covers uselang,
//and extensions for auto-detecting user language.
- $ulang = $wgLang->getCode();
+ $ulang = $wgLang->getCode();
$clang = $wgContLang->getCode();
$cacheable = $wgUseFileCache
@@ -2814,6 +2998,8 @@ class Article {
$revision->insertOn( $dbw );
$this->updateRevisionOn( $dbw, $revision );
$dbw->commit();
+
+ wfRunHooks( 'NewRevisionFromEditComplete', array($this, $revision, false) );
wfProfileOut( __METHOD__ );
}
@@ -2911,6 +3097,15 @@ class Article {
static function onArticleDelete( $title ) {
global $wgUseFileCache, $wgMessageCache;
+ // Update existence markers on article/talk tabs...
+ if( $title->isTalkPage() ) {
+ $other = $title->getSubjectPage();
+ } else {
+ $other = $title->getTalkPage();
+ }
+ $other->invalidateCache();
+ $other->purgeSquid();
+
$title->touchLinks();
$title->purgeSquid();
@@ -2920,13 +3115,20 @@ class Article {
@unlink( $cm->fileCacheName() );
}
- if( $title->getNamespace() == NS_MEDIAWIKI) {
+ # Messages
+ if( $title->getNamespace() == NS_MEDIAWIKI ) {
$wgMessageCache->replace( $title->getDBkey(), false );
}
+ # Images
if( $title->getNamespace() == NS_IMAGE ) {
$update = new HTMLCacheUpdate( $title, 'imagelinks' );
$update->doUpdate();
}
+ # User talk pages
+ if( $title->getNamespace() == NS_USER_TALK ) {
+ $user = User::newFromName( $title->getText(), false );
+ $user->setNewtalk( false );
+ }
}
/**
@@ -2940,7 +3142,7 @@ class Article {
// Invalidate the caches of all pages which redirect here
$wgDeferredUpdateList[] = new HTMLCacheUpdate( $title, 'redirect' );
-
+
# Purge squid for this page only
$title->purgeSquid();
@@ -2954,6 +3156,15 @@ class Article {
/**#@-*/
/**
+ * Overriden by ImagePage class, only present here to avoid a fatal error
+ * Called for ?action=revert
+ */
+ public function revert(){
+ global $wgOut;
+ $wgOut->showErrorPage( 'nosuchaction', 'nosuchactiontext' );
+ }
+
+ /**
* Info about this page
* Called for ?action=info when $wgAllowPageInfo is on.
*
@@ -3080,38 +3291,33 @@ class Article {
}
/**
- * Return an auto-generated summary if the text provided is a redirect.
+ * Returns a list of hidden categories this page is a member of.
+ * Uses the page_props and categorylinks tables.
*
- * @param string $text The wikitext to check
- * @return string '' or an appropriate summary
+ * @return array Array of Title objects
*/
- public static function getRedirectAutosummary( $text ) {
- $rt = Title::newFromRedirect( $text );
- if( is_object( $rt ) )
- return wfMsgForContent( 'autoredircomment', $rt->getFullText() );
- else
- return '';
- }
+ function getHiddenCategories() {
+ $result = array();
+ $id = $this->mTitle->getArticleID();
+ if( $id == 0 ) {
+ return array();
+ }
- /**
- * Return an auto-generated summary if the new text is much shorter than
- * the old text.
- *
- * @param string $oldtext The previous text of the page
- * @param string $text The submitted text of the page
- * @return string An appropriate autosummary, or an empty string.
- */
- public static function getBlankingAutosummary( $oldtext, $text ) {
- if ($oldtext!='' && $text=='') {
- return wfMsgForContent('autosumm-blank');
- } elseif (strlen($oldtext) > 10 * strlen($text) && strlen($text) < 500) {
- #Removing more than 90% of the article
- global $wgContLang;
- $truncatedtext = $wgContLang->truncate($text, max(0, 200 - strlen(wfMsgForContent('autosumm-replace'))), '...');
- return wfMsgForContent('autosumm-replace', $truncatedtext);
- } else {
- return '';
+ $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 );
+ }
+ }
}
+ $dbr->freeResult( $res );
+ return $result;
}
/**
@@ -3122,38 +3328,42 @@ class Article {
* @return string An appropriate autosummary, or an empty string.
*/
public static function getAutosummary( $oldtext, $newtext, $flags ) {
+ # Decide what kind of autosummary is needed.
- # This code is UGLY UGLY UGLY.
- # Somebody PLEASE come up with a more elegant way to do it.
-
- #Redirect autosummaries
- $summary = self::getRedirectAutosummary( $newtext );
-
- if ($summary)
- return $summary;
-
- #Blanking autosummaries
- if (!($flags & EDIT_NEW))
- $summary = self::getBlankingAutosummary( $oldtext, $newtext );
-
- if ($summary)
- return $summary;
+ # Redirect autosummaries
+ $rt = Title::newFromRedirect( $newtext );
+ if( is_object( $rt ) ) {
+ return wfMsgForContent( 'autoredircomment', $rt->getFullText() );
+ }
- #New page autosummaries
- if ($flags & EDIT_NEW && strlen($newtext)) {
- #If they're making a new article, give its text, truncated, in the summary.
+ # New page autosummaries
+ if( $flags & EDIT_NEW && strlen( $newtext ) ) {
+ # If they're making a new article, give its text, truncated, in the summary.
global $wgContLang;
$truncatedtext = $wgContLang->truncate(
str_replace("\n", ' ', $newtext),
max( 0, 200 - strlen( wfMsgForContent( 'autosumm-new') ) ),
'...' );
- $summary = wfMsgForContent( 'autosumm-new', $truncatedtext );
+ return wfMsgForContent( 'autosumm-new', $truncatedtext );
}
- if ($summary)
- return $summary;
+ # Blanking autosummaries
+ if( $oldtext != '' && $newtext == '' ) {
+ return wfMsgForContent('autosumm-blank');
+ } elseif( strlen( $oldtext ) > 10 * strlen( $newtext ) && strlen( $newtext ) < 500) {
+ # Removing more than 90% of the article
+ global $wgContLang;
+ $truncatedtext = $wgContLang->truncate(
+ $newtext,
+ max( 0, 200 - strlen( wfMsgForContent( 'autosumm-replace' ) ) ),
+ '...'
+ );
+ return wfMsgForContent( 'autosumm-replace', $truncatedtext );
+ }
- return $summary;
+ # If we reach this point, there's no applicable autosummary for our case, so our
+ # autosummary is empty.
+ return '';
}
/**
@@ -3175,7 +3385,7 @@ class Article {
$popts->setTidy(false);
$popts->enableLimitReport( false );
if ( $wgEnableParserCache && $cache && $this && $parserOutput->getCacheTime() != -1 ) {
- $parserCache =& ParserCache::singleton();
+ $parserCache = ParserCache::singleton();
$parserCache->save( $parserOutput, $this, $wgUser );
}
@@ -3235,4 +3445,60 @@ class Article {
$wgOut->addParserOutput( $parserOutput );
}
+ /**
+ * Update all the appropriate counts in the category table, given that
+ * we've added the categories $added and deleted the categories $deleted.
+ *
+ * @param $added array The names of categories that were added
+ * @param $deleted array The names of categories that were deleted
+ * @return null
+ */
+ public function updateCategoryCounts( $added, $deleted ) {
+ $ns = $this->mTitle->getNamespace();
+ $dbw = wfGetDB( DB_MASTER );
+
+ # First make sure the rows exist. If one of the "deleted" ones didn't
+ # exist, we might legitimately not create it, but it's simpler to just
+ # create it and then give it a negative value, since the value is bogus
+ # anyway.
+ #
+ # Sometimes I wish we had INSERT ... ON DUPLICATE KEY UPDATE.
+ $insertCats = array_merge( $added, $deleted );
+ if( !$insertCats ) {
+ # Okay, nothing to do
+ return;
+ }
+ $insertRows = array();
+ foreach( $insertCats as $cat ) {
+ $insertRows[] = array( 'cat_title' => $cat );
+ }
+ $dbw->insert( 'category', $insertRows, __METHOD__, 'IGNORE' );
+
+ $addFields = array( 'cat_pages = cat_pages + 1' );
+ $removeFields = array( 'cat_pages = cat_pages - 1' );
+ if( $ns == NS_CATEGORY ) {
+ $addFields[] = 'cat_subcats = cat_subcats + 1';
+ $removeFields[] = 'cat_subcats = cat_subcats - 1';
+ } elseif( $ns == NS_IMAGE ) {
+ $addFields[] = 'cat_files = cat_files + 1';
+ $removeFields[] = 'cat_files = cat_files - 1';
+ }
+
+ if ( $added ) {
+ $dbw->update(
+ 'category',
+ $addFields,
+ array( 'cat_title' => $added ),
+ __METHOD__
+ );
+ }
+ if ( $deleted ) {
+ $dbw->update(
+ 'category',
+ $removeFields,
+ array( 'cat_title' => $deleted ),
+ __METHOD__
+ );
+ }
+ }
}
diff --git a/includes/AuthPlugin.php b/includes/AuthPlugin.php
index 2ad137e2..7717e001 100644
--- a/includes/AuthPlugin.php
+++ b/includes/AuthPlugin.php
@@ -230,7 +230,7 @@ class AuthPlugin {
* @param $autocreate bool True if user is being autocreated on login
* @public
*/
- function initUser( $user, $autocreate=false ) {
+ function initUser( &$user, $autocreate=false ) {
# Override this to do something.
}
@@ -242,5 +242,3 @@ class AuthPlugin {
return $username;
}
}
-
-
diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php
index 2e2083b2..4f36784a 100644
--- a/includes/AutoLoader.php
+++ b/includes/AutoLoader.php
@@ -4,352 +4,265 @@
ini_set('unserialize_callback_func', '__autoload' );
-function __autoload($className) {
- global $wgAutoloadClasses;
-
+class AutoLoader {
# Locations of core classes
# Extension classes are specified with $wgAutoloadClasses
static $localClasses = array(
# Includes
'AjaxDispatcher' => 'includes/AjaxDispatcher.php',
- 'AjaxCachePolicy' => 'includes/AjaxFunctions.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',
- 'HashBagOStuff' => 'includes/BagOStuff.php',
- 'SqlBagOStuff' => 'includes/BagOStuff.php',
- 'MediaWikiBagOStuff' => 'includes/BagOStuff.php',
- 'TurckBagOStuff' => 'includes/BagOStuff.php',
- 'APCBagOStuff' => 'includes/BagOStuff.php',
- 'eAccelBagOStuff' => 'includes/BagOStuff.php',
- 'XCacheBagOStuff' => 'includes/BagOStuff.php',
- 'DBABagOStuff' => 'includes/BagOStuff.php',
'Block' => 'includes/Block.php',
- 'HTMLFileCache' => 'includes/HTMLFileCache.php',
- 'DependencyWrapper' => 'includes/CacheDependency.php',
- 'FileDependency' => 'includes/CacheDependency.php',
- 'TitleDependency' => 'includes/CacheDependency.php',
- 'TitleListDependency' => 'includes/CacheDependency.php',
+ 'CacheDependency' => 'includes/CacheDependency.php',
+ 'Category' => 'includes/Category.php',
+ 'Categoryfinder' => 'includes/Categoryfinder.php',
'CategoryPage' => 'includes/CategoryPage.php',
'CategoryViewer' => 'includes/CategoryPage.php',
- 'Categoryfinder' => 'includes/Categoryfinder.php',
- 'RCCacheEntry' => 'includes/ChangesList.php',
'ChangesList' => 'includes/ChangesList.php',
- 'OldChangesList' => 'includes/ChangesList.php',
- 'EnhancedChangesList' => 'includes/ChangesList.php',
- 'CoreParserFunctions' => 'includes/CoreParserFunctions.php',
- 'DBObject' => 'includes/Database.php',
- 'Database' => 'includes/Database.php',
- 'DatabaseMysql' => 'includes/Database.php',
- 'ResultWrapper' => 'includes/Database.php',
- 'DatabasePostgres' => 'includes/DatabasePostgres.php',
- 'DatabaseOracle' => 'includes/DatabaseOracle.php',
- 'DateFormatter' => 'includes/DateFormatter.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',
- '_DiffOp' => 'includes/DifferenceEngine.php',
- '_DiffOp_Copy' => 'includes/DifferenceEngine.php',
- '_DiffOp_Delete' => 'includes/DifferenceEngine.php',
+ 'DiffFormatter' => 'includes/DifferenceEngine.php',
+ 'Diff' => 'includes/DifferenceEngine.php',
'_DiffOp_Add' => 'includes/DifferenceEngine.php',
'_DiffOp_Change' => 'includes/DifferenceEngine.php',
- '_DiffEngine' => 'includes/DifferenceEngine.php',
- 'Diff' => 'includes/DifferenceEngine.php',
- 'MappedDiff' => 'includes/DifferenceEngine.php',
- 'DiffFormatter' => 'includes/DifferenceEngine.php',
- 'UnifiedDiffFormatter' => 'includes/DifferenceEngine.php',
- 'ArrayDiffFormatter' => 'includes/DifferenceEngine.php',
+ '_DiffOp_Copy' => 'includes/DifferenceEngine.php',
+ '_DiffOp_Delete' => 'includes/DifferenceEngine.php',
+ '_DiffOp' => 'includes/DifferenceEngine.php',
'DjVuImage' => 'includes/DjVuImage.php',
- '_HWLDF_WordAccumulator' => 'includes/DifferenceEngine.php',
- 'WordLevelDiff' => 'includes/DifferenceEngine.php',
- 'TableDiffFormatter' => 'includes/DifferenceEngine.php',
- 'EditPage' => 'includes/EditPage.php',
- 'MWException' => 'includes/Exception.php',
- 'Exif' => 'includes/Exif.php',
- 'FormatExif' => 'includes/Exif.php',
- 'WikiExporter' => 'includes/Export.php',
- 'XmlDumpWriter' => 'includes/Export.php',
- 'DumpOutput' => 'includes/Export.php',
- 'DumpFileOutput' => 'includes/Export.php',
- 'DumpPipeOutput' => 'includes/Export.php',
- 'DumpGZipOutput' => 'includes/Export.php',
- 'DumpBZip2Output' => 'includes/Export.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',
- 'DumpNotalkFilter' => 'includes/Export.php',
- 'DumpNamespaceFilter' => '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',
- 'ExternalStore' => 'includes/ExternalStore.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',
- 'ChannelFeed' => 'includes/Feed.php',
- 'RSSFeed' => 'includes/Feed.php',
- 'AtomFeed' => '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',
- 'ConcatenatedGzipHistoryBlob' => 'includes/HistoryBlob.php',
'HistoryBlobStub' => 'includes/HistoryBlob.php',
- 'HistoryBlobCurStub' => 'includes/HistoryBlob.php',
'HTMLCacheUpdate' => 'includes/HTMLCacheUpdate.php',
+ 'HTMLCacheUpdateJob' => 'includes/HTMLCacheUpdate.php',
+ 'HTMLFileCache' => 'includes/HTMLFileCache.php',
'Http' => 'includes/HttpFunctions.php',
- 'IP' => 'includes/IP.php',
+ '_HWLDF_WordAccumulator' => 'includes/DifferenceEngine.php',
'ImageGallery' => 'includes/ImageGallery.php',
- 'ImagePage' => 'includes/ImagePage.php',
'ImageHistoryList' => 'includes/ImagePage.php',
- 'FileDeleteForm' => 'includes/FileDeleteForm.php',
- 'FileRevertForm' => 'includes/FileRevertForm.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',
- 'EmaillingJob' => 'includes/EmaillingJob.php',
- 'EnotifNotifyJob' => 'includes/EnotifNotifyJob.php',
- 'HTMLCacheUpdateJob' => 'includes/HTMLCacheUpdate.php',
- 'RefreshLinksJob' => 'includes/RefreshLinksJob.php',
- 'Licenses' => 'includes/Licenses.php',
'License' => 'includes/Licenses.php',
+ 'Licenses' => 'includes/Licenses.php',
'LinkBatch' => 'includes/LinkBatch.php',
'LinkCache' => 'includes/LinkCache.php',
- 'LinkFilter' => 'includes/LinkFilter.php',
'Linker' => 'includes/Linker.php',
+ 'LinkFilter' => 'includes/LinkFilter.php',
'LinksUpdate' => 'includes/LinksUpdate.php',
- 'LoadBalancer' => 'includes/LoadBalancer.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',
- 'MagicWord' => 'includes/MagicWord.php',
'MagicWordArray' => 'includes/MagicWord.php',
+ 'MagicWord' => 'includes/MagicWord.php',
+ 'MailAddress' => 'includes/UserMailer.php',
+ 'MappedDiff' => 'includes/DifferenceEngine.php',
'MathRenderer' => 'includes/Math.php',
- 'MediaTransformOutput' => 'includes/MediaTransformOutput.php',
- 'ThumbnailImage' => 'includes/MediaTransformOutput.php',
'MediaTransformError' => 'includes/MediaTransformOutput.php',
- 'TransformParameterError' => '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',
- 'Namespace' => 'includes/Namespace.php',
- 'FakeMemCachedClient' => 'includes/ObjectCache.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',
- 'IndexPager' => 'includes/Pager.php',
- 'ReverseChronologicalPager' => 'includes/Pager.php',
- 'TablePager' => 'includes/Pager.php',
- 'Parser' => 'includes/Parser.php',
- 'Parser_OldPP' => 'includes/Parser_OldPP.php',
- 'Parser_DiffTest' => 'includes/Parser_DiffTest.php',
- 'ParserCache' => 'includes/ParserCache.php',
- 'ParserOutput' => 'includes/ParserOutput.php',
- 'ParserOptions' => 'includes/ParserOptions.php',
+ 'PageHistoryPager' => 'includes/PageHistory.php',
+ 'PageQueryPage' => 'includes/PageQueryPage.php',
+ 'Pager' => 'includes/Pager.php',
+ 'PasswordError' => 'includes/User.php',
'PatrolLog' => 'includes/PatrolLog.php',
- 'Preprocessor' => 'includes/Preprocessor.php',
+ 'PostgresSearchResult' => 'includes/SearchPostgres.php',
+ 'PostgresSearchResultSet' => 'includes/SearchPostgres.php',
'PrefixSearch' => 'includes/PrefixSearch.php',
- 'PPFrame' => 'includes/Preprocessor.php',
- 'PPNode' => 'includes/Preprocessor.php',
- 'Preprocessor_DOM' => 'includes/Preprocessor_DOM.php',
- 'PPFrame_DOM' => 'includes/Preprocessor_DOM.php',
- 'PPTemplateFrame_DOM' => 'includes/Preprocessor_DOM.php',
- 'PPDStack' => 'includes/Preprocessor_DOM.php',
- 'PPDStackElement' => 'includes/Preprocessor_DOM.php',
- 'PPNode_DOM' => 'includes/Preprocessor_DOM.php',
- 'Preprocessor_Hash' => 'includes/Preprocessor_Hash.php',
+ 'Profiler' => 'includes/Profiler.php',
'ProfilerSimple' => 'includes/ProfilerSimple.php',
+ 'ProfilerSimpleText' => 'includes/ProfilerSimpleText.php',
'ProfilerSimpleUDP' => 'includes/ProfilerSimpleUDP.php',
- 'Profiler' => 'includes/Profiler.php',
- 'ProxyTools' => 'includes/ProxyTools.php',
'ProtectionForm' => 'includes/ProtectionForm.php',
'QueryPage' => 'includes/QueryPage.php',
- 'PageQueryPage' => 'includes/PageQueryPage.php',
- 'ImageQueryPage' => 'includes/ImageQueryPage.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',
- 'SearchEngine' => 'includes/SearchEngine.php',
- 'SearchResultSet' => 'includes/SearchEngine.php',
- 'SearchResult' => 'includes/SearchEngine.php',
'SearchEngineDummy' => 'includes/SearchEngine.php',
- 'SearchMySQL' => 'includes/SearchMySQL.php',
- 'MySQLSearchResultSet' => 'includes/SearchMySQL.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',
- 'SearchOracle' => 'includes/SearchOracle.php',
'SiteConfiguration' => 'includes/SiteConfiguration.php',
'SiteStats' => 'includes/SiteStats.php',
'SiteStatsUpdate' => 'includes/SiteStats.php',
'Skin' => 'includes/Skin.php',
- 'MediaWiki_I18N' => 'includes/SkinTemplate.php',
'SkinTemplate' => 'includes/SkinTemplate.php',
- 'QuickTemplate' => 'includes/SkinTemplate.php',
- 'SpecialAllpages' => 'includes/SpecialAllpages.php',
- 'AncientPagesPage' => 'includes/SpecialAncientpages.php',
- 'IPBlockForm' => 'includes/SpecialBlockip.php',
- 'SpecialBookSources' => 'includes/SpecialBooksources.php',
- 'BrokenRedirectsPage' => 'includes/SpecialBrokenRedirects.php',
- 'EmailConfirmation' => 'includes/SpecialConfirmemail.php',
- 'ContributionsPage' => 'includes/SpecialContributions.php',
- 'DeadendPagesPage' => 'includes/SpecialDeadendpages.php',
- 'DisambiguationsPage' => 'includes/SpecialDisambiguations.php',
- 'DoubleRedirectsPage' => 'includes/SpecialDoubleRedirects.php',
- 'EmailUserForm' => 'includes/SpecialEmailuser.php',
- 'WikiRevision' => 'includes/SpecialImport.php',
- 'WikiImporter' => 'includes/SpecialImport.php',
- 'ImportStringSource' => 'includes/SpecialImport.php',
- 'ImportStreamSource' => 'includes/SpecialImport.php',
- 'IPUnblockForm' => 'includes/SpecialIpblocklist.php',
- 'ListredirectsPage' => 'includes/SpecialListredirects.php',
- 'DBLockForm' => 'includes/SpecialLockdb.php',
- 'LogReader' => 'includes/SpecialLog.php',
- 'LogViewer' => 'includes/SpecialLog.php',
- 'LonelyPagesPage' => 'includes/SpecialLonelypages.php',
- 'LongPagesPage' => 'includes/SpecialLongpages.php',
- 'MIMEsearchPage' => 'includes/SpecialMIMEsearch.php',
- 'MostcategoriesPage' => 'includes/SpecialMostcategories.php',
- 'MostimagesPage' => 'includes/SpecialMostimages.php',
- 'MostlinkedPage' => 'includes/SpecialMostlinked.php',
- 'MostlinkedCategoriesPage' => 'includes/SpecialMostlinkedcategories.php',
- 'SpecialMostlinkedtemplates' => 'includes/SpecialMostlinkedtemplates.php',
- 'MostrevisionsPage' => 'includes/SpecialMostrevisions.php',
- 'FewestrevisionsPage' => 'includes/SpecialFewestrevisions.php',
- 'MovePageForm' => 'includes/SpecialMovepage.php',
- 'NewbieContributionsPage' => 'includes/SpecialNewbieContributions.php',
- 'NewPagesPage' => 'includes/SpecialNewpages.php',
+ 'SpecialMycontributions' => 'includes/SpecialPage.php',
+ 'SpecialMypage' => 'includes/SpecialPage.php',
+ 'SpecialMytalk' => 'includes/SpecialPage.php',
'SpecialPage' => 'includes/SpecialPage.php',
- 'UnlistedSpecialPage' => 'includes/SpecialPage.php',
- 'IncludableSpecialPage' => 'includes/SpecialPage.php',
- 'PopularPagesPage' => 'includes/SpecialPopularpages.php',
- 'PreferencesForm' => 'includes/SpecialPreferences.php',
- 'SpecialPrefixindex' => 'includes/SpecialPrefixindex.php',
- 'RandomPage' => 'includes/SpecialRandompage.php',
- 'SpecialRandomredirect' => 'includes/SpecialRandomredirect.php',
- 'PasswordResetForm' => 'includes/SpecialResetpass.php',
- 'RevisionDeleteForm' => 'includes/SpecialRevisiondelete.php',
- 'RevisionDeleter' => 'includes/SpecialRevisiondelete.php',
- 'SpecialSearch' => 'includes/SpecialSearch.php',
- 'ShortPagesPage' => 'includes/SpecialShortpages.php',
- 'UncategorizedCategoriesPage' => 'includes/SpecialUncategorizedcategories.php',
- 'UncategorizedPagesPage' => 'includes/SpecialUncategorizedpages.php',
- 'UncategorizedTemplatesPage' => 'includes/SpecialUncategorizedtemplates.php',
- 'PageArchive' => 'includes/SpecialUndelete.php',
- 'UndeleteForm' => 'includes/SpecialUndelete.php',
- 'DBUnlockForm' => 'includes/SpecialUnlockdb.php',
- 'UnusedCategoriesPage' => 'includes/SpecialUnusedcategories.php',
- 'UnusedimagesPage' => 'includes/SpecialUnusedimages.php',
- 'UnusedtemplatesPage' => 'includes/SpecialUnusedtemplates.php',
- 'UnwatchedpagesPage' => 'includes/SpecialUnwatchedpages.php',
- 'UploadForm' => 'includes/SpecialUpload.php',
- 'UploadFormMogile' => 'includes/SpecialUploadMogile.php',
- 'LoginForm' => 'includes/SpecialUserlogin.php',
- 'UserrightsPage' => 'includes/SpecialUserrights.php',
- 'SpecialVersion' => 'includes/SpecialVersion.php',
- 'WantedCategoriesPage' => 'includes/SpecialWantedcategories.php',
- 'WantedPagesPage' => 'includes/SpecialWantedpages.php',
- 'WhatLinksHerePage' => 'includes/SpecialWhatlinkshere.php',
- 'WithoutInterwikiPage' => 'includes/SpecialWithoutinterwiki.php',
+ 'SpecialRedirectToSpecial' => 'includes/SpecialPage.php',
+ 'SqlBagOStuff' => 'includes/BagOStuff.php',
'SquidUpdate' => 'includes/SquidUpdate.php',
- 'ReplacementArray' => 'includes/StringUtils.php',
- 'Replacer' => 'includes/StringUtils.php',
- 'RegexlikeReplacer' => 'includes/StringUtils.php',
- 'DoubleReplacer' => 'includes/StringUtils.php',
- 'HashtableReplacer' => 'includes/StringUtils.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',
- 'UserRightsProxy' => 'includes/UserRightsProxy.php',
- 'MailAddress' => 'includes/UserMailer.php',
- 'EmailNotification' => 'includes/UserMailer.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',
- 'FauxRequest' => 'includes/WebRequest.php',
- 'MediaWiki' => 'includes/Wiki.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',
- 'memcached' => 'includes/memcached-client.php',
- 'EmaillingJob' => 'includes/JobQueue.php',
- 'WatchlistEditor' => 'includes/WatchlistEditor.php',
- # filerepo
- 'ArchivedFile' => 'includes/filerepo/ArchivedFile.php',
- 'File' => 'includes/filerepo/File.php',
- 'FileRepo' => 'includes/filerepo/FileRepo.php',
- 'FileRepoStatus' => 'includes/filerepo/FileRepoStatus.php',
- 'ForeignDBFile' => 'includes/filerepo/ForeignDBFile.php',
- 'ForeignDBRepo' => 'includes/filerepo/ForeignDBRepo.php',
- 'FSRepo' => 'includes/filerepo/FSRepo.php',
- 'Image' => 'includes/filerepo/LocalFile.php',
- 'LocalFile' => 'includes/filerepo/LocalFile.php',
- 'LocalFileDeleteBatch' => 'includes/filerepo/LocalFile.php',
- 'LocalFileRestoreBatch' => 'includes/filerepo/LocalFile.php',
- 'LocalRepo' => 'includes/filerepo/LocalRepo.php',
- 'OldLocalFile' => 'includes/filerepo/OldLocalFile.php',
- 'RepoGroup' => 'includes/filerepo/RepoGroup.php',
- 'UnregisteredLocalFile' => 'includes/filerepo/UnregisteredLocalFile.php',
-
- # Media
- 'BitmapHandler' => 'includes/media/Bitmap.php',
- 'BmpHandler' => 'includes/media/BMP.php',
- 'DjVuHandler' => 'includes/media/DjVu.php',
- 'MediaHandler' => 'includes/media/Generic.php',
- 'ImageHandler' => 'includes/media/Generic.php',
- 'SvgHandler' => 'includes/media/SVG.php',
-
- # Normal
- 'UtfNormal' => 'includes/normal/UtfNormal.php',
-
- # Templates
- 'UsercreateTemplate' => 'includes/templates/Userlogin.php',
- 'UserloginTemplate' => 'includes/templates/Userlogin.php',
-
- # Languages
- 'Language' => 'languages/Language.php',
-
- # API
+ # 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',
- 'ApiFormatFeedWrapper' => 'includes/api/ApiFormatBase.php',
'ApiFeedWatchlist' => 'includes/api/ApiFeedWatchlist.php',
'ApiFormatBase' => 'includes/api/ApiFormatBase.php',
- 'Services_JSON' => 'includes/api/ApiFormatJson_json.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',
- 'ApiFormatTxt' => 'includes/api/ApiFormatTxt.php',
- 'ApiFormatDbg' => 'includes/api/ApiFormatDbg.php',
- 'Spyc' => 'includes/api/ApiFormatYaml_spyc.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',
- 'ApiQueryAllpages' => 'includes/api/ApiQueryAllpages.php',
- 'ApiQueryAllLinks' => 'includes/api/ApiQueryAllLinks.php',
'ApiQueryAllCategories' => 'includes/api/ApiQueryAllCategories.php',
+ 'ApiQueryAllimages' => 'includes/api/ApiQueryAllimages.php',
+ 'ApiQueryAllLinks' => 'includes/api/ApiQueryAllLinks.php',
'ApiQueryAllUsers' => 'includes/api/ApiQueryAllUsers.php',
- 'ApiQueryBase' => 'includes/api/ApiQueryBase.php',
- 'ApiQueryGeneratorBase' => 'includes/api/ApiQueryBase.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',
- 'ApiQueryExternalLinks' => 'includes/api/ApiQueryExternalLinks.php',
+ 'ApiQueryDeletedrevs' => 'includes/api/ApiQueryDeletedrevs.php',
'ApiQueryExtLinksUsage' => 'includes/api/ApiQueryExtLinksUsage.php',
- 'ApiQueryImages' => 'includes/api/ApiQueryImages.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',
@@ -358,72 +271,264 @@ function __autoload($className) {
'ApiQueryRecentChanges'=> 'includes/api/ApiQueryRecentChanges.php',
'ApiQueryRevisions' => 'includes/api/ApiQueryRevisions.php',
'ApiQuerySearch' => 'includes/api/ApiQuerySearch.php',
- 'ApiQueryAllmessages' => 'includes/api/ApiQueryAllmessages.php',
'ApiQuerySiteinfo' => 'includes/api/ApiQuerySiteinfo.php',
- 'ApiQueryUsers' => 'includes/api/ApiQueryUsers.php',
'ApiQueryUserInfo' => 'includes/api/ApiQueryUserInfo.php',
+ 'ApiQueryUsers' => 'includes/api/ApiQueryUsers.php',
'ApiQueryWatchlist' => 'includes/api/ApiQueryWatchlist.php',
'ApiResult' => 'includes/api/ApiResult.php',
-
- # apiedit branch
- 'ApiBlock' => 'includes/api/ApiBlock.php',
- #'ApiChangeRights' => 'includes/api/ApiChangeRights.php',
- # Disabled for now
- 'ApiDelete' => 'includes/api/ApiDelete.php',
- 'ApiMove' => 'includes/api/ApiMove.php',
- 'ApiProtect' => 'includes/api/ApiProtect.php',
- 'ApiQueryBlocks' => 'includes/api/ApiQueryBlocks.php',
- 'ApiQueryDeletedrevs' => 'includes/api/ApiQueryDeletedrevs.php',
'ApiRollback' => 'includes/api/ApiRollback.php',
'ApiUnblock' => 'includes/api/ApiUnblock.php',
- 'ApiUndelete' => 'includes/api/ApiUndelete.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/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/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/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/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/templates
+ 'UsercreateTemplate' => 'includes/templates/Userlogin.php',
+ 'UserloginTemplate' => 'includes/templates/Userlogin.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',
+
);
-
- wfProfileIn( __METHOD__ );
- if ( isset( $localClasses[$className] ) ) {
- $filename = $localClasses[$className];
- } elseif ( isset( $wgAutoloadClasses[$className] ) ) {
- $filename = $wgAutoloadClasses[$className];
- } else {
- # Try a different capitalisation
- # The case can sometimes be wrong when unserializing PHP 4 objects
- $filename = false;
- $lowerClass = strtolower( $className );
- foreach ( $localClasses as $class2 => $file2 ) {
- if ( strtolower( $class2 ) == $lowerClass ) {
- $filename = $file2;
+
+ /**
+ * 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.
+ */
+ static function autoload( $className ) {
+ global $wgAutoloadClasses;
+
+ wfProfileIn( __METHOD__ );
+ if ( isset( self::$localClasses[$className] ) ) {
+ $filename = self::$localClasses[$className];
+ } elseif ( isset( $wgAutoloadClasses[$className] ) ) {
+ $filename = $wgAutoloadClasses[$className];
+ } else {
+ # Try a different capitalisation
+ # The case can sometimes be wrong when unserializing PHP 4 objects
+ $filename = false;
+ $lowerClass = strtolower( $className );
+ foreach ( self::$localClasses as $class2 => $file2 ) {
+ if ( strtolower( $class2 ) == $lowerClass ) {
+ $filename = $file2;
+ }
+ }
+ if ( !$filename ) {
+ # Give up
+ wfProfileOut( __METHOD__ );
+ return false;
}
}
- if ( !$filename ) {
- # Give up
- wfProfileOut( __METHOD__ );
- return;
+
+ # Make an absolute path, this improves performance by avoiding some stat calls
+ if ( substr( $filename, 0, 1 ) != '/' && substr( $filename, 1, 1 ) != ':' ) {
+ global $IP;
+ $filename = "$IP/$filename";
}
+ require( $filename );
+ wfProfileOut( __METHOD__ );
+ return true;
}
- # Make an absolute path, this improves performance by avoiding some stat calls
- if ( substr( $filename, 0, 1 ) != '/' && substr( $filename, 1, 1 ) != ':' ) {
- global $IP;
- $filename = "$IP/$filename";
+ static function loadAllExtensions() {
+ global $wgAutoloadClasses;
+
+ foreach( $wgAutoloadClasses as $class => $file ) {
+ if( !( class_exists( $class ) || interface_exists( $class ) ) ) {
+ require( $file );
+ }
+ }
}
- require( $filename );
- wfProfileOut( __METHOD__ );
}
function wfLoadAllExtensions() {
- global $wgAutoloadClasses;
+ AutoLoader::loadAllExtensions();
+}
- # It is crucial that SpecialPage.php is included before any special page
- # extensions are loaded. Otherwise the parent class will not be available
- # when APC loads the early-bound extension class. Normally this is
- # guaranteed by entering special pages via SpecialPage members such as
- # executePath(), but here we have to take a more explicit measure.
-
- require_once( dirname(__FILE__) . '/SpecialPage.php' );
-
- foreach( $wgAutoloadClasses as $class => $file ) {
- if( !( class_exists( $class ) || interface_exists( $class ) ) ) {
- require( $file );
- }
+if ( function_exists( 'spl_autoload_register' ) ) {
+ spl_autoload_register( array( 'AutoLoader', 'autoload' ) );
+} else {
+ function __autoload( $class ) {
+ AutoLoader::autoload( $class );
}
}
+
diff --git a/includes/Autopromote.php b/includes/Autopromote.php
index b5097423..68fe6636 100644
--- a/includes/Autopromote.php
+++ b/includes/Autopromote.php
@@ -8,7 +8,7 @@ class Autopromote {
/**
* Get the groups for the given user based on $wgAutopromote.
*
- * @param User $user The user to get the groups for
+ * @param $user The user to get the groups for
* @return array Array of groups to promote to.
*/
public static function getAutopromoteGroups( User $user ) {
@@ -18,6 +18,9 @@ class Autopromote {
if( self::recCheckCondition( $cond, $user ) )
$promote[] = $group;
}
+
+ wfRunHooks( 'GetAutoPromoteGroups', array($user, &$promote) );
+
return $promote;
}
@@ -33,12 +36,12 @@ class Autopromote {
* This function evaluates the former type recursively, and passes off to
* self::checkCondition for evaluation of the latter type.
*
- * @param mixed $cond A condition, possibly containing other conditions
- * @param User $user The user to check the conditions against
+ * @param $cond Mixed: a condition, possibly containing other conditions
+ * @param $user The user to check the conditions against
* @return bool Whether the condition is true
*/
private static function recCheckCondition( $cond, User $user ) {
- $validOps = array( '&', '|', '^' );
+ $validOps = array( '&', '|', '^', '!' );
if( is_array( $cond ) && count( $cond ) >= 2 && in_array( $cond[0], $validOps ) ) {
# Recursive condition
if( $cond[0] == '&' ) {
@@ -47,7 +50,7 @@ class Autopromote {
return false;
return true;
} elseif( $cond[0] == '|' ) {
- foreach( array_slice( $cond, 1 ) as $subcond )
+ foreach( array_slice( $cond, 1 ) as $subcond )
if( self::recCheckCondition( $subcond, $user ) )
return true;
return false;
@@ -60,6 +63,11 @@ class Autopromote {
$res = ($res xor self::recCheckCondition( $subcond, $user ));
}
return $res;
+ } elseif ( $cond[0] = '!' ) {
+ foreach( array_slice( $cond, 1 ) as $subcond )
+ if( self::recCheckCondition( $subcond, $user ) )
+ return false;
+ return true;
}
}
# If we got here, the array presumably does not contain other condi-
@@ -75,8 +83,8 @@ class Autopromote {
* APCOND_AGE. Other types will throw an exception if no extension evalu-
* ates them.
*
- * @param array $cond A condition, which must not contain other conditions
- * @param User $user The user to check the condition against
+ * @param $cond Array: A condition, which must not contain other conditions
+ * @param $user The user to check the condition against
* @return bool Whether the condition is true for the user
*/
private static function checkCondition( $cond, User $user ) {
@@ -87,7 +95,7 @@ class Autopromote {
if( User::isValidEmailAddr( $user->getEmail() ) ) {
global $wgEmailAuthentication;
if( $wgEmailAuthentication ) {
- return $user->getEmailAuthenticationTimestamp() ? true : false;
+ return (bool)$user->getEmailAuthenticationTimestamp();
} else {
return true;
}
diff --git a/includes/BagOStuff.php b/includes/BagOStuff.php
index 226abb35..b4fefc97 100644
--- a/includes/BagOStuff.php
+++ b/includes/BagOStuff.php
@@ -17,22 +17,25 @@
# 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
+
/**
+ * @defgroup Cache Cache
*
+ * @file
+ * @ingroup Cache
*/
/**
- * Simple generic object store
- *
* interface is intended to be more or less compatible with
* the PHP memcached client.
*
* backends for local hash array and SQL table included:
* <code>
* $bag = new HashBagOStuff();
- * $bag = new MysqlBagOStuff($tablename); # connect to db first
+ * $bag = new MediaWikiBagOStuff($tablename); # connect to db first
* </code>
*
+ * @ingroup Cache
*/
class BagOStuff {
var $debugmode;
@@ -167,14 +170,12 @@ class BagOStuff {
/**
* Functional versions!
- * @todo document
+ * This is a test of the interface, mainly. It stores things in an associative
+ * array, which is not going to persist between program runs.
+ *
+ * @ingroup Cache
*/
class HashBagOStuff extends BagOStuff {
- /*
- This is a test of the interface, mainly. It stores
- things in an associative array, which is not going to
- persist between program runs.
- */
var $bag;
function __construct() {
@@ -213,24 +214,20 @@ class HashBagOStuff extends BagOStuff {
}
}
-/*
-CREATE TABLE objectcache (
- keyname char(255) binary not null default '',
- value mediumblob,
- exptime datetime,
- unique key (keyname),
- key (exptime)
-);
-*/
-
/**
- * @todo document
- * @abstract
+ * Generic class to store objects in a database
+ *
+ * @ingroup Cache
*/
abstract class SqlBagOStuff extends BagOStuff {
var $table;
var $lastexpireall = 0;
+ /**
+ * Constructor
+ *
+ * @param $tablename String: name of the table to use
+ */
function __construct($tablename = 'objectcache') {
$this->table = $tablename;
}
@@ -247,8 +244,8 @@ abstract class SqlBagOStuff extends BagOStuff {
}
if($row=$this->_fetchobject($res)) {
$this->_debug("get: retrieved data; exp time is " . $row->exptime);
- if ( $row->exptime != $this->_maxdatetime() &&
- wfTimestamp( TS_UNIX, $row->exptime ) < time() )
+ if ( $row->exptime != $this->_maxdatetime() &&
+ wfTimestamp( TS_UNIX, $row->exptime ) < time() )
{
$this->_debug("get: key has expired, deleting");
$this->delete($key);
@@ -262,7 +259,7 @@ abstract class SqlBagOStuff extends BagOStuff {
}
function set($key,$value,$exptime=0) {
- if ( wfReadOnly() ) {
+ if ( $this->_readonly() ) {
return false;
}
$exptime = intval($exptime);
@@ -284,7 +281,7 @@ abstract class SqlBagOStuff extends BagOStuff {
}
function delete($key,$time=0) {
- if ( wfReadOnly() ) {
+ if ( $this->_readonly() ) {
return false;
}
$this->_query(
@@ -340,6 +337,8 @@ abstract class SqlBagOStuff extends BagOStuff {
abstract function _doinsert($table, $vals);
abstract function _doquery($sql);
+ abstract function _readonly();
+
function _freeresult($result) {
/* stub */
return false;
@@ -367,7 +366,7 @@ abstract class SqlBagOStuff extends BagOStuff {
function expireall() {
/* Remove any items that have expired */
- if ( wfReadOnly() ) {
+ if ( $this->_readonly() ) {
return false;
}
$now = $this->_fromunixtime( time() );
@@ -376,7 +375,7 @@ abstract class SqlBagOStuff extends BagOStuff {
function deleteall() {
/* Clear *all* items from cache table */
- if ( wfReadOnly() ) {
+ if ( $this->_readonly() ) {
return false;
}
$this->_query( "DELETE FROM $0" );
@@ -387,7 +386,7 @@ abstract class SqlBagOStuff extends BagOStuff {
* On typical message and page data, this can provide a 3X decrease
* in storage requirements.
*
- * @param mixed $data
+ * @param $data mixed
* @return string
*/
function _serialize( &$data ) {
@@ -401,7 +400,7 @@ abstract class SqlBagOStuff extends BagOStuff {
/**
* Unserialize and, if necessary, decompress an object.
- * @param string $serial
+ * @param $serial string
* @return mixed
*/
function _unserialize( $serial ) {
@@ -417,31 +416,33 @@ abstract class SqlBagOStuff extends BagOStuff {
}
/**
- * @todo document
+ * Stores objects in the main database of the wiki
+ *
+ * @ingroup Cache
*/
class MediaWikiBagOStuff extends SqlBagOStuff {
var $tableInitialised = false;
+ function _getDB(){
+ static $db;
+ if( !isset( $db ) )
+ $db = wfGetDB( DB_MASTER );
+ return $db;
+ }
function _doquery($sql) {
- $dbw = wfGetDB( DB_MASTER );
- return $dbw->query($sql, 'MediaWikiBagOStuff::_doquery');
+ return $this->_getDB()->query( $sql, __METHOD__ );
}
function _doinsert($t, $v) {
- $dbw = wfGetDB( DB_MASTER );
- return $dbw->insert($t, $v, 'MediaWikiBagOStuff::_doinsert',
- array( 'IGNORE' ) );
+ return $this->_getDB()->insert($t, $v, __METHOD__, array( 'IGNORE' ) );
}
function _fetchobject($result) {
- $dbw = wfGetDB( DB_MASTER );
- return $dbw->fetchObject($result);
+ return $this->_getDB()->fetchObject($result);
}
function _freeresult($result) {
- $dbw = wfGetDB( DB_MASTER );
- return $dbw->freeResult($result);
+ return $this->_getDB()->freeResult($result);
}
function _dberror($result) {
- $dbw = wfGetDB( DB_MASTER );
- return $dbw->lastError();
+ return $this->_getDB()->lastError();
}
function _maxdatetime() {
if ( time() > 0x7fffffff ) {
@@ -451,24 +452,23 @@ class MediaWikiBagOStuff extends SqlBagOStuff {
}
}
function _fromunixtime($ts) {
- $dbw = wfGetDB(DB_MASTER);
- return $dbw->timestamp($ts);
+ return $this->_getDB()->timestamp($ts);
+ }
+ function _readonly(){
+ return wfReadOnly();
}
function _strencode($s) {
- $dbw = wfGetDB( DB_MASTER );
- return $dbw->strencode($s);
+ return $this->_getDB()->strencode($s);
}
function _blobencode($s) {
- $dbw = wfGetDB( DB_MASTER );
- return $dbw->encodeBlob($s);
+ return $this->_getDB()->encodeBlob($s);
}
function _blobdecode($s) {
- $dbw = wfGetDB( DB_MASTER );
- return $dbw->decodeBlob($s);
+ return $this->_getDB()->decodeBlob($s);
}
function getTableName() {
if ( !$this->tableInitialised ) {
- $dbw = wfGetDB( DB_MASTER );
+ $dbw = $this->_getDB();
/* This is actually a hack, we should be able
to use Language classes here... or not */
if (!$dbw)
@@ -493,6 +493,7 @@ class MediaWikiBagOStuff extends SqlBagOStuff {
* that Turck's serializer is faster, so a possible future extension would be
* to use it for arrays but not for objects.
*
+ * @ingroup Cache
*/
class TurckBagOStuff extends BagOStuff {
function get($key) {
@@ -527,6 +528,7 @@ class TurckBagOStuff extends BagOStuff {
/**
* This is a wrapper for APC's shared memory functions
*
+ * @ingroup Cache
*/
class APCBagOStuff extends BagOStuff {
function get($key) {
@@ -536,12 +538,12 @@ class APCBagOStuff extends BagOStuff {
}
return $val;
}
-
+
function set($key, $value, $exptime=0) {
apc_store($key, serialize($value), $exptime);
return true;
}
-
+
function delete($key, $time=0) {
apc_delete($key);
return true;
@@ -555,6 +557,7 @@ class APCBagOStuff extends BagOStuff {
* This is basically identical to the Turck MMCache version,
* mostly because eAccelerator is based on Turck MMCache.
*
+ * @ingroup Cache
*/
class eAccelBagOStuff extends BagOStuff {
function get($key) {
@@ -589,13 +592,15 @@ class eAccelBagOStuff extends BagOStuff {
/**
* Wrapper for XCache object caching functions; identical interface
* to the APC wrapper
+ *
+ * @ingroup Cache
*/
class XCacheBagOStuff extends BagOStuff {
/**
* Get a value from the XCache object cache
*
- * @param string $key Cache key
+ * @param $key String: cache key
* @return mixed
*/
public function get( $key ) {
@@ -604,40 +609,41 @@ class XCacheBagOStuff extends BagOStuff {
$val = unserialize( $val );
return $val;
}
-
+
/**
* Store a value in the XCache object cache
*
- * @param string $key Cache key
- * @param mixed $value Object to store
- * @param int $expire Expiration time
+ * @param $key String: cache key
+ * @param $value Mixed: object to store
+ * @param $expire Int: expiration time
* @return bool
*/
public function set( $key, $value, $expire = 0 ) {
xcache_set( $key, serialize( $value ), $expire );
return true;
}
-
+
/**
* Remove a value from the XCache object cache
*
- * @param string $key Cache key
- * @param int $time Not used in this implementation
+ * @param $key String: cache key
+ * @param $time Int: not used in this implementation
* @return bool
*/
public function delete( $key, $time = 0 ) {
xcache_unset( $key );
return true;
}
-
+
}
/**
* @todo document
+ * @ingroup Cache
*/
class DBABagOStuff extends BagOStuff {
var $mHandler, $mFile, $mReader, $mWriter, $mDisabled;
-
+
function __construct( $handler = 'db3', $dir = false ) {
if ( $dir === false ) {
global $wgTmpDirectory;
@@ -645,6 +651,7 @@ class DBABagOStuff extends BagOStuff {
}
$this->mFile = "$dir/mw-cache-" . wfWikiID();
$this->mFile .= '.db';
+ wfDebug( __CLASS__.": using cache file {$this->mFile}\n" );
$this->mHandler = $handler;
}
@@ -664,7 +671,7 @@ class DBABagOStuff extends BagOStuff {
if ( !is_string( $blob ) ) {
return array( null, 0 );
} else {
- return array(
+ return array(
unserialize( substr( $blob, 11 ) ),
intval( substr( $blob, 0, 10 ) )
);
@@ -779,5 +786,3 @@ class DBABagOStuff extends BagOStuff {
return $result;
}
}
-
-
diff --git a/includes/Block.php b/includes/Block.php
index 3688d7cf..b208fa8a 100644
--- a/includes/Block.php
+++ b/includes/Block.php
@@ -1,5 +1,6 @@
<?php
/**
+ * @file
* Blocks and bans object
*/
@@ -15,16 +16,16 @@
class Block
{
/* public*/ var $mAddress, $mUser, $mBy, $mReason, $mTimestamp, $mAuto, $mId, $mExpiry,
- $mRangeStart, $mRangeEnd, $mAnonOnly, $mEnableAutoblock, $mHideName,
- $mBlockEmail;
- /* private */ var $mNetworkBits, $mIntegerAddr, $mForUpdate, $mFromMaster, $mByName;
-
+ $mRangeStart, $mRangeEnd, $mAnonOnly, $mEnableAutoblock, $mHideName,
+ $mBlockEmail, $mByName, $mAngryAutoblock;
+ /* private */ var $mNetworkBits, $mIntegerAddr, $mForUpdate, $mFromMaster;
+
const EB_KEEP_EXPIRED = 1;
const EB_FOR_UPDATE = 2;
const EB_RANGE_ONLY = 4;
function __construct( $address = '', $user = 0, $by = 0, $reason = '',
- $timestamp = '' , $auto = 0, $expiry = '', $anonOnly = 0, $createAccount = 0, $enableAutoblock = 0,
+ $timestamp = '' , $auto = 0, $expiry = '', $anonOnly = 0, $createAccount = 0, $enableAutoblock = 0,
$hideName = 0, $blockEmail = 0 )
{
$this->mId = 0;
@@ -45,6 +46,7 @@ class Block
$this->mForUpdate = false;
$this->mFromMaster = false;
$this->mByName = false;
+ $this->mAngryAutoblock = false;
$this->initialiseRange();
}
@@ -59,10 +61,10 @@ class Block
}
}
- static function newFromID( $id )
+ static function newFromID( $id )
{
$dbr = wfGetDB( DB_SLAVE );
- $res = $dbr->resultObject( $dbr->select( 'ipblocks', '*',
+ $res = $dbr->resultObject( $dbr->select( 'ipblocks', '*',
array( 'ipb_id' => $id ), __METHOD__ ) );
$block = new Block;
if ( $block->loadFromResult( $res ) ) {
@@ -75,8 +77,8 @@ class Block
function clear()
{
$this->mAddress = $this->mReason = $this->mTimestamp = '';
- $this->mId = $this->mAnonOnly = $this->mCreateAccount =
- $this->mEnableAutoblock = $this->mAuto = $this->mUser =
+ $this->mId = $this->mAnonOnly = $this->mCreateAccount =
+ $this->mEnableAutoblock = $this->mAuto = $this->mUser =
$this->mBy = $this->mHideName = $this->mBlockEmail = 0;
$this->mByName = false;
}
@@ -124,7 +126,7 @@ class Block
# Try user block
if ( $user ) {
- $res = $db->resultObject( $db->select( 'ipblocks', '*', array( 'ipb_user' => $user ),
+ $res = $db->resultObject( $db->select( 'ipblocks', '*', array( 'ipb_user' => $user ),
__METHOD__, $options ) );
if ( $this->loadFromResult( $res, $killExpired ) ) {
return true;
@@ -170,7 +172,7 @@ class Block
return true;
}
}
-
+
# Give up
$this->clear();
return false;
@@ -179,7 +181,7 @@ class Block
/**
* Fill in member variables from a result wrapper
*/
- function loadFromResult( ResultWrapper $res, $killExpired = true )
+ function loadFromResult( ResultWrapper $res, $killExpired = true )
{
$ret = false;
if ( 0 != $res->numRows() ) {
@@ -234,7 +236,7 @@ class Block
"ipb_range_start <= '$iaddr'",
"ipb_range_end >= '$iaddr'"
);
-
+
if ( $user ) {
$conds['ipb_anon_only'] = 0;
}
@@ -270,7 +272,7 @@ class Block
if ( isset( $row->user_name ) ) {
$this->mByName = $row->user_name;
} else {
- $this->mByName = false;
+ $this->mByName = $row->ipb_by_text;
}
$this->mRangeStart = $row->ipb_range_start;
$this->mRangeEnd = $row->ipb_range_end;
@@ -358,7 +360,7 @@ class Block
/**
* Insert a block into the block table.
- *@return Whether or not the insertion was successful.
+ * @return Whether or not the insertion was successful.
*/
function insert()
{
@@ -376,6 +378,15 @@ class Block
$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();
+ }
+ }
+
# Don't collide with expired blocks
Block::purgeExpired();
@@ -386,6 +397,7 @@ class Block
'ipb_address' => $this->mAddress,
'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,
@@ -400,7 +412,6 @@ class Block
), 'Block::insert', array( 'IGNORE' )
);
$affected = $dbw->affectedRows();
- $dbw->commit();
if ($affected)
$this->doRetroactiveAutoblock();
@@ -420,17 +431,30 @@ class Block
if ($this->mEnableAutoblock && $this->mUser) {
wfDebug("Doing retroactive autoblocks for " . $this->mAddress . "\n");
+
+ $options = array( 'ORDER BY' => 'rc_timestamp DESC' );
+ $conds = array( 'rc_user_text' => $this->mAddress );
+
+ if ($this->mAngryAutoblock) {
+ // Block any IP used in the last 7 days. Up to five IPs.
+ $conds[] = 'rc_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( time() - (7*86400) ) );
+ $options['LIMIT'] = 5;
+ } else {
+ // Just the last IP used.
+ $options['LIMIT'] = 1;
+ }
- $row = $dbr->selectRow( 'recentchanges', array( 'rc_ip' ), array( 'rc_user_text' => $this->mAddress ),
- __METHOD__ , array( 'ORDER BY' => 'rc_timestamp DESC' ) );
+ $res = $dbr->select( 'recentchanges', array( 'rc_ip' ), $conds,
+ __METHOD__ , $options);
- if ( !$row || !$row->rc_ip ) {
+ if ( !$dbr->numRows( $res ) ) {
#No results, don't autoblock anything
wfDebug("No IP found to retroactively autoblock\n");
} else {
- #Limit is 1, so no loop needed.
- $retroblockip = $row->rc_ip;
- return $this->doAutoblock( $retroblockip, true );
+ while ( $row = $dbr->fetchObject( $res ) ) {
+ if ( $row->rc_ip )
+ $this->doAutoblock( $row->rc_ip );
+ }
}
}
}
@@ -476,6 +500,12 @@ class Block
wfDebug( " No match\n" );
}
}
+
+ ## Allow hooks to cancel the autoblock.
+ 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.
@@ -502,6 +532,7 @@ class Block
$ipblock->mAddress = $autoblockip;
$ipblock->mUser = 0;
$ipblock->mBy = $this->mBy;
+ $ipblock->mByName = $this->mByName;
$ipblock->mReason = wfMsgForContent( 'autoblocker', $this->mAddress, $this->mReason );
$ipblock->mTimestamp = wfTimestampNow();
$ipblock->mAuto = 1;
@@ -592,9 +623,6 @@ class Block
*/
function getByName()
{
- if ( $this->mByName === false ) {
- $this->mByName = User::whoIs( $this->mBy );
- }
return $this->mByName;
}
@@ -613,7 +641,7 @@ class Block
return $this->mAddress;
}
}
-
+
/**
* Encode expiry for DB
*/
@@ -625,7 +653,7 @@ class Block
}
}
- /**
+ /**
* Decode expiry which has come from the DB
*/
static function decodeExpiry( $expiry, $timestampType = TS_MW ) {
@@ -635,14 +663,14 @@ class Block
return wfTimestamp( $timestampType, $expiry );
}
}
-
+
static function getAutoblockExpiry( $timestamp )
{
global $wgAutoblockExpiry;
return wfTimestamp( TS_MW, wfTimestamp( TS_UNIX, $timestamp ) + $wgAutoblockExpiry );
}
-
- /**
+
+ /**
* Gets rid of uneeded numbers in quad-dotted/octet IP strings
* For example, 127.111.113.151/24 -> 127.111.113.0/24
*/
@@ -675,7 +703,7 @@ class Block
return $range;
}
- /**
+ /**
* Purge expired blocks from the ipblocks table
*/
static function purgeExpired() {
@@ -684,8 +712,8 @@ class Block
}
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.
+ # This is a special keyword for timestamps in PostgreSQL, and
+ # works with CHAR(14) as well because "i" sorts after all numbers.
return 'infinity';
/*
@@ -697,6 +725,48 @@ class Block
return $infinity;
*/
}
+
+ /**
+ * Convert a DB-encoded expiry into a real string that humans can read.
+ */
+ static function formatExpiry( $encoded_expiry ) {
+
+ static $msg = null;
+
+ if( is_null( $msg ) ) {
+ $msg = array();
+ $keys = array( 'infiniteblock', 'expiringblock' );
+ foreach( $keys as $key ) {
+ $msg[$key] = wfMsgHtml( $key );
+ }
+ }
+
+ $expiry = Block::decodeExpiry( $encoded_expiry );
+ if ($expiry == 'infinity') {
+ $expirystr = $msg['infiniteblock'];
+ } else {
+ global $wgLang;
+ $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.
+ */
+ static function parseExpiryInput( $expiry_input ) {
+ if ( $expiry_input == 'infinite' || $expiry_input == 'indefinite' ) {
+ $expiry = 'infinity';
+ } else {
+ $expiry = strtotime( $expiry_input );
+ if ($expiry < 0 || $expiry === false) {
+ return false;
+ }
+ }
+
+ return $expiry;
+ }
}
-
diff --git a/includes/CacheDependency.php b/includes/CacheDependency.php
index 1d48c383..b050c46d 100644
--- a/includes/CacheDependency.php
+++ b/includes/CacheDependency.php
@@ -1,10 +1,10 @@
<?php
/**
- * This class stores an arbitrary value along with its dependencies.
+ * This class stores an arbitrary value along with its dependencies.
* Users should typically only use DependencyWrapper::getFromCache(), rather
* than instantiating one of these objects directly.
- * @addtogroup Cache
+ * @ingroup Cache
*/
class DependencyWrapper {
var $value;
@@ -24,7 +24,7 @@ class DependencyWrapper {
$this->deps = $deps;
}
- /**
+ /**
* Returns true if any of the dependencies have expired
*/
function isExpired() {
@@ -62,24 +62,24 @@ class DependencyWrapper {
}
/**
- * Attempt to get a value from the cache. If the value is expired or missing,
+ * Attempt to get a value from the cache. If the value is expired or missing,
* it will be generated with the callback function (if present), and the newly
- * calculated value will be stored to the cache in a wrapper.
+ * calculated value will be stored to the cache in a wrapper.
*
* @param object $cache A cache object such as $wgMemc
* @param string $key The cache key
* @param integer $expiry The expiry timestamp or interval in seconds
* @param mixed $callback The callback for generating the value, or false
* @param array $callbackParams The function parameters for the callback
- * @param array $deps The dependencies to store on a cache miss. Note: these
+ * @param array $deps The dependencies to store on a cache miss. Note: these
* are not the dependencies used on a cache hit! Cache hits use the stored
* dependency array.
*
* @return mixed The value, or null if it was not present in the cache and no
* callback was defined.
*/
- static function getValueFromCache( $cache, $key, $expiry = 0, $callback = false,
- $callbackParams = array(), $deps = array() )
+ static function getValueFromCache( $cache, $key, $expiry = 0, $callback = false,
+ $callbackParams = array(), $deps = array() )
{
$obj = $cache->get( $key );
if ( is_object( $obj ) && $obj instanceof DependencyWrapper && !$obj->isExpired() ) {
@@ -97,7 +97,7 @@ class DependencyWrapper {
}
/**
- * @addtogroup Cache
+ * @ingroup Cache
*/
abstract class CacheDependency {
/**
@@ -112,7 +112,7 @@ abstract class CacheDependency {
}
/**
- * @addtogroup Cache
+ * @ingroup Cache
*/
class FileDependency extends CacheDependency {
var $filename, $timestamp;
@@ -122,11 +122,11 @@ class FileDependency extends CacheDependency {
*
* @param string $filename The name of the file, preferably fully qualified
* @param mixed $timestamp The unix last modified timestamp, or false if the
- * file does not exist. If omitted, the timestamp will be loaded from
+ * file does not exist. If omitted, the timestamp will be loaded from
* the file.
*
- * A dependency on a nonexistent file will be triggered when the file is
- * created. A dependency on an existing file will be triggered when the
+ * A dependency on a nonexistent file will be triggered when the file is
+ * created. A dependency on an existing file will be triggered when the
* file is changed.
*/
function __construct( $filename, $timestamp = null ) {
@@ -171,7 +171,7 @@ class FileDependency extends CacheDependency {
}
/**
- * @addtogroup Cache
+ * @ingroup Cache
*/
class TitleDependency extends CacheDependency {
var $titleObj;
@@ -191,7 +191,7 @@ class TitleDependency extends CacheDependency {
function loadDependencyValues() {
$this->touched = $this->getTitle()->getTouched();
}
-
+
/**
* Get rid of bulky Title object for sleep
*/
@@ -202,7 +202,7 @@ class TitleDependency extends CacheDependency {
function getTitle() {
if ( !isset( $this->titleObj ) ) {
$this->titleObj = Title::makeTitle( $this->ns, $this->dbk );
- }
+ }
return $this->titleObj;
}
@@ -230,12 +230,12 @@ class TitleDependency extends CacheDependency {
}
/**
- * @addtogroup Cache
+ * @ingroup Cache
*/
class TitleListDependency extends CacheDependency {
var $linkBatch;
var $timestamps;
-
+
/**
* Construct a dependency on a list of titles
*/
@@ -259,7 +259,7 @@ class TitleListDependency extends CacheDependency {
if ( count( $timestamps ) ) {
$dbr = wfGetDB( DB_SLAVE );
$where = $this->getLinkBatch()->constructSet( 'page', $dbr );
- $res = $dbr->select( 'page',
+ $res = $dbr->select( 'page',
array( 'page_namespace', 'page_title', 'page_touched' ),
$where, __METHOD__ );
while ( $row = $dbr->fetchObject( $res ) ) {
@@ -313,11 +313,11 @@ class TitleListDependency extends CacheDependency {
}
/**
- * @addtogroup Cache
+ * @ingroup Cache
*/
class GlobalDependency extends CacheDependency {
var $name, $value;
-
+
function __construct( $name ) {
$this->name = $name;
$this->value = $GLOBALS[$name];
@@ -329,7 +329,7 @@ class GlobalDependency extends CacheDependency {
}
/**
- * @addtogroup Cache
+ * @ingroup Cache
*/
class ConstantDependency extends CacheDependency {
var $name, $value;
@@ -343,5 +343,3 @@ class ConstantDependency extends CacheDependency {
return constant( $this->name ) != $this->value;
}
}
-
-
diff --git a/includes/Category.php b/includes/Category.php
new file mode 100644
index 00000000..acafc47a
--- /dev/null
+++ b/includes/Category.php
@@ -0,0 +1,261 @@
+<?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.
+ *
+ * TODO: Move some stuff from CategoryPage.php to here, and use that.
+ *
+ * @author Simetrical
+ */
+
+class Category {
+ /** Name of the category, normalized to DB-key form */
+ private $mName = null;
+ private $mID = null;
+ /** Category page title */
+ private $mTitle = null;
+ /** Counts of membership (cat_pages, cat_subcats, cat_files) */
+ private $mPages = null, $mSubcats = null, $mFiles = null;
+
+ private function __construct() {}
+
+ /**
+ * Set up all member variables using a database query.
+ * @return bool True on success, false on failure.
+ */
+ protected function initialize() {
+ if ( $this->mName === null && $this->mTitle )
+ $this->mName = $title->getDBKey();
+
+ if( $this->mName === null && $this->mID === null ) {
+ throw new MWException( __METHOD__.' has both names and IDs null' );
+ } elseif( $this->mID === null ) {
+ $where = array( 'cat_title' => $this->mName );
+ } elseif( $this->mName === null ) {
+ $where = array( 'cat_id' => $this->mID );
+ } else {
+ # Already initialized
+ return true;
+ }
+ $dbr = wfGetDB( DB_SLAVE );
+ $row = $dbr->selectRow(
+ 'category',
+ array( 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats',
+ 'cat_files' ),
+ $where,
+ __METHOD__
+ );
+ if( !$row ) {
+ # Okay, there were no contents. Nothing to initialize.
+ if ( $this->mTitle ) {
+ # If there is a title object but no record in the category table, treat this as an empty category
+ $this->mID = false;
+ $this->mName = $this->mTitle->getDBKey();
+ $this->mPages = 0;
+ $this->mSubcats = 0;
+ $this->mFiles = 0;
+
+ return true;
+ } else {
+ return false; # Fail
+ }
+ }
+ $this->mID = $row->cat_id;
+ $this->mName = $row->cat_title;
+ $this->mPages = $row->cat_pages;
+ $this->mSubcats = $row->cat_subcats;
+ $this->mFiles = $row->cat_files;
+
+ # (bug 13683) If the count is negative, then 1) it's obviously wrong
+ # and should not be kept, and 2) we *probably* don't have to scan many
+ # rows to obtain the correct figure, so let's risk a one-time recount.
+ if( $this->mPages < 0 || $this->mSubcats < 0 ||
+ $this->mFiles < 0 ) {
+ $this->refreshCounts();
+ }
+
+ return true;
+ }
+
+ /**
+ * Factory function.
+ *
+ * @param array $name 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
+ */
+ public static function newFromName( $name ) {
+ $cat = new self();
+ $title = Title::makeTitleSafe( NS_CATEGORY, $name );
+ if( !is_object( $title ) ) {
+ return false;
+ }
+
+ $cat->mTitle = $title;
+ $cat->mName = $title->getDBKey();
+
+ return $cat;
+ }
+
+ /**
+ * Factory function.
+ *
+ * @param array $title Title for the category page
+ * @return mixed Category, or false on a totally invalid name
+ */
+ public static function newFromTitle( $title ) {
+ $cat = new self();
+
+ $cat->mTitle = $title;
+ $cat->mName = $title->getDBKey();
+
+ return $cat;
+ }
+
+ /**
+ * Factory function.
+ *
+ * @param array $id A category id
+ * @return Category
+ */
+ public static function newFromID( $id ) {
+ $cat = new self();
+ $cat->mID = intval( $id );
+ return $cat;
+ }
+
+ /**
+ * Factory function, for constructing a Category object from a result set
+ *
+ * @param $row result set row, must contain the cat_xxx fields. If the fields are null,
+ * the resulting Category object will represent an empty category if a title object
+ * was given. If the fields are null and no title was given, this method fails and returns false.
+ * @param $title optional title object for the category represented by the given row.
+ * May be provided if it is already known, to avoid having to re-create a title object later.
+ * @return Category
+ */
+ public static function newFromRow( $row, $title = null ) {
+ $cat = new self();
+ $cat->mTitle = $title;
+
+
+ # NOTE: the row often results from a LEFT JOIN on categorylinks. This may result in
+ # all the cat_xxx fields being null, if the category page exists, but nothing
+ # was ever added to the category. This case should be treated linke an empty
+ # category, if possible.
+
+ if ( $row->cat_title === null ) {
+ if ( $title === null ) {
+ # the name is probably somewhere in the row, for example as page_title,
+ # but we can't know that here...
+ return false;
+ } else {
+ $cat->mName = $title->getDBKey(); # if we have a title object, fetch the category name from there
+ }
+
+ $cat->mID = false;
+ $cat->mSubcats = 0;
+ $cat->mPages = 0;
+ $cat->mFiles = 0;
+ } else {
+ $cat->mName = $row->cat_title;
+ $cat->mID = $row->cat_id;
+ $cat->mSubcats = $row->cat_subcats;
+ $cat->mPages = $row->cat_pages;
+ $cat->mFiles = $row->cat_files;
+ }
+
+ return $cat;
+ }
+
+ /** @return mixed DB key name, or false on failure */
+ public function getName() { return $this->getX( 'mName' ); }
+ /** @return mixed Category ID, or false on failure */
+ public function getID() { return $this->getX( 'mID' ); }
+ /** @return mixed Total number of member pages, or false on failure */
+ public function getPageCount() { return $this->getX( 'mPages' ); }
+ /** @return mixed Number of subcategories, or false on failure */
+ public function getSubcatCount() { return $this->getX( 'mSubcats' ); }
+ /** @return mixed Number of member files, or false on failure */
+ public function getFileCount() { return $this->getX( 'mFiles' ); }
+
+ /**
+ * @return mixed The Title for this category, or false on failure.
+ */
+ public function getTitle() {
+ if( $this->mTitle ) return $this->mTitle;
+
+ if( !$this->initialize() ) {
+ return false;
+ }
+
+ $this->mTitle = Title::makeTitleSafe( NS_CATEGORY, $this->mName );
+ return $this->mTitle;
+ }
+
+ /** Generic accessor */
+ private function getX( $key ) {
+ if( !$this->initialize() ) {
+ return false;
+ }
+ return $this->{$key};
+ }
+
+ /**
+ * Refresh the counts for this category.
+ *
+ * @return bool True on success, false on failure
+ */
+ public function refreshCounts() {
+ if( wfReadOnly() ) {
+ return false;
+ }
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->begin();
+ # Note, we must use names for this, since categorylinks does.
+ if( $this->mName === null ) {
+ if( !$this->initialize() ) {
+ return false;
+ }
+ } else {
+ # Let's be sure that the row exists in the table. We don't need to
+ # do this if we got the row from the table in initialization!
+ $dbw->insert(
+ 'category',
+ array( 'cat_title' => $this->mName ),
+ __METHOD__,
+ 'IGNORE'
+ );
+ }
+
+ $cond1 = $dbw->conditional( 'page_namespace='.NS_CATEGORY, 1, 'NULL' );
+ $cond2 = $dbw->conditional( 'page_namespace='.NS_IMAGE, 1, 'NULL' );
+ $result = $dbw->selectRow(
+ array( 'categorylinks', 'page' ),
+ array( 'COUNT(*) AS pages',
+ "COUNT($cond1) AS subcats",
+ "COUNT($cond2) AS files"
+ ),
+ array( 'cl_to' => $this->mName, 'page_id = cl_from' ),
+ __METHOD__,
+ 'LOCK IN SHARE MODE'
+ );
+ $ret = $dbw->update(
+ 'category',
+ array(
+ 'cat_pages' => $result->pages,
+ 'cat_subcats' => $result->subcats,
+ 'cat_files' => $result->files
+ ),
+ array( 'cat_title' => $this->mName ),
+ __METHOD__
+ );
+ $dbw->commit();
+
+ # Now we should update our local counts.
+ $this->mPages = $result->pages;
+ $this->mSubcats = $result->subcats;
+ $this->mFiles = $result->files;
+
+ return $ret;
+ }
+}
diff --git a/includes/CategoryPage.php b/includes/CategoryPage.php
index 6fbcd3c1..92e4e279 100644
--- a/includes/CategoryPage.php
+++ b/includes/CategoryPage.php
@@ -66,10 +66,12 @@ class CategoryPage extends Article {
class CategoryViewer {
var $title, $limit, $from, $until,
- $articles, $articles_start_char,
+ $articles, $articles_start_char,
$children, $children_start_char,
$showGallery, $gallery,
$skin;
+ /** Category object for this page */
+ private $cat;
function __construct( $title, $from = '', $until = '' ) {
global $wgCategoryPagingLimit;
@@ -77,8 +79,9 @@ class CategoryViewer {
$this->from = $from;
$this->until = $until;
$this->limit = $wgCategoryPagingLimit;
+ $this->cat = Category::newFromName( $title->getDBKey() );
}
-
+
/**
* Format the category data list.
*
@@ -132,12 +135,21 @@ class CategoryViewer {
}
/**
- * Add a subcategory to the internal lists
+ * Add a subcategory to the internal lists, using a Category object
+ */
+ function addSubcategoryObject( $cat, $sortkey, $pageLength ) {
+ $title = $cat->getTitle();
+ $this->addSubcategory( $title, $sortkey, $pageLength );
+ }
+
+ /**
+ * Add a subcategory to the internal lists, using a title object
+ * @deprectated kept for compatibility, please use addSubcategoryObject instead
*/
function addSubcategory( $title, $sortkey, $pageLength ) {
global $wgContLang;
// Subcategory; strip the 'Category' namespace from the link text.
- $this->children[] = $this->getSkin()->makeKnownLinkObj(
+ $this->children[] = $this->getSkin()->makeKnownLinkObj(
$title, $wgContLang->convertHtml( $title->getText() ) );
$this->children_start_char[] = $this->getSubcategorySortChar( $title, $sortkey );
@@ -152,13 +164,13 @@ class CategoryViewer {
*/
function getSubcategorySortChar( $title, $sortkey ) {
global $wgContLang;
-
+
if( $title->getPrefixedText() == $sortkey ) {
$firstChar = $wgContLang->firstChar( $title->getDBkey() );
} else {
$firstChar = $wgContLang->firstChar( $sortkey );
}
-
+
return $wgContLang->convert( $firstChar );
}
@@ -167,11 +179,10 @@ class CategoryViewer {
*/
function addImage( Title $title, $sortkey, $pageLength, $isRedirect = false ) {
if ( $this->showGallery ) {
- $image = new Image( $title );
if( $this->flip ) {
- $this->gallery->insert( $image );
+ $this->gallery->insert( $title );
} else {
- $this->gallery->add( $image );
+ $this->gallery->add( $title );
}
} else {
$this->addPage( $title, $sortkey, $pageLength, $isRedirect );
@@ -211,17 +222,17 @@ class CategoryViewer {
$this->flip = false;
}
$res = $dbr->select(
- array( 'page', 'categorylinks' ),
- array( 'page_title', 'page_namespace', 'page_len', 'page_is_redirect', 'cl_sortkey' ),
+ 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_from = page_id',
- 'cl_to' => $this->title->getDBkey()),
- #'page_is_redirect' => 0),
- #+ $pageCondition,
+ 'cl_to' => $this->title->getDBkey() ),
__METHOD__,
array( 'ORDER BY' => $this->flip ? 'cl_sortkey DESC' : 'cl_sortkey',
- 'USE INDEX' => '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 ) ) );
$count = 0;
$this->nextPage = null;
@@ -236,7 +247,8 @@ class CategoryViewer {
$title = Title::makeTitle( $x->page_namespace, $x->page_title );
if( $title->getNamespace() == NS_CATEGORY ) {
- $this->addSubcategory( $title, $x->cl_sortkey, $x->page_len );
+ $cat = Category::newFromRow( $x, $title );
+ $this->addSubcategoryObject( $cat, $x->cl_sortkey, $x->page_len );
} elseif( $this->showGallery && $title->getNamespace() == NS_IMAGE ) {
$this->addImage( $title, $x->cl_sortkey, $x->page_len, $x->page_is_redirect );
} else {
@@ -261,12 +273,14 @@ class CategoryViewer {
function getSubcategorySection() {
# Don't show subcategories section if there are none.
$r = '';
- $c = count( $this->children );
- if( $c > 0 ) {
+ $rescnt = count( $this->children );
+ $dbcnt = $this->cat->getSubcatCount();
+ $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'subcat' );
+ if( $rescnt > 0 ) {
# Showing subcategories
$r .= "<div id=\"mw-subcategories\">\n";
$r .= '<h2>' . wfMsg( 'subcategories' ) . "</h2>\n";
- $r .= wfMsgExt( 'subcategorycount', array( 'parse' ), $c );
+ $r .= $countmsg;
$r .= $this->formatList( $this->children, $this->children_start_char );
$r .= "\n</div>";
}
@@ -277,11 +291,20 @@ class CategoryViewer {
$ti = htmlspecialchars( $this->title->getText() );
# Don't show articles section if there are none.
$r = '';
- $c = count( $this->articles );
- if( $c > 0 ) {
+
+ # FIXME, here and in the other two sections: we don't need to bother
+ # with this rigamarole if the entire category contents fit on one page
+ # and have already been retrieved. We can just use $rescnt in that
+ # case and save a query and some logic.
+ $dbcnt = $this->cat->getPageCount() - $this->cat->getSubcatCount()
+ - $this->cat->getFileCount();
+ $rescnt = count( $this->articles );
+ $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'article' );
+
+ if( $rescnt > 0 ) {
$r = "<div id=\"mw-pages\">\n";
$r .= '<h2>' . wfMsg( 'category_header', $ti ) . "</h2>\n";
- $r .= wfMsgExt( 'categoryarticlecount', array( 'parse' ), $c );
+ $r .= $countmsg;
$r .= $this->formatList( $this->articles, $this->articles_start_char );
$r .= "\n</div>";
}
@@ -290,10 +313,13 @@ class CategoryViewer {
function getImageSection() {
if( $this->showGallery && ! $this->gallery->isEmpty() ) {
+ $dbcnt = $this->cat->getFileCount();
+ $rescnt = $this->gallery->count();
+ $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'file' );
+
return "<div id=\"mw-category-media\">\n" .
'<h2>' . wfMsg( 'category-media-header', htmlspecialchars($this->title->getText()) ) . "</h2>\n" .
- wfMsgExt( 'category-media-count', array( 'parse' ), $this->gallery->count() ) .
- $this->gallery->toHTML() . "\n</div>";
+ $countmsg . $this->gallery->toHTML() . "\n</div>";
} else {
return '';
}
@@ -440,7 +466,48 @@ class CategoryViewer {
return "($prevLink) ($nextLink)";
}
-}
-
-
+ /**
+ * What to do if the category table conflicts with the number of results
+ * returned? This function says what. It works the same whether the
+ * things being counted are articles, subcategories, or files.
+ *
+ * Note for grepping: uses the messages category-article-count,
+ * category-article-count-limited, category-subcat-count,
+ * 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.
+ */
+ private function getCountMessage( $rescnt, $dbcnt, $type ) {
+ global $wgLang;
+ # There are three cases:
+ # 1) The category table figure seems sane. It might be wrong, but
+ # we can't do anything about it if we don't recalculate it on ev-
+ # ery category view.
+ # 2) The category table figure isn't sane, like it's smaller than the
+ # number of actual results, *but* the number of results is less
+ # than $this->limit and there's no offset. In this case we still
+ # know the right figure.
+ # 3) We have no idea.
+ $totalrescnt = count( $this->articles ) + count( $this->children ) +
+ ($this->showGallery ? $this->gallery->count() : 0);
+ if($dbcnt == $rescnt || (($totalrescnt == $this->limit || $this->from
+ || $this->until) && $dbcnt > $rescnt)){
+ # Case 1: seems sane.
+ $totalcnt = $dbcnt;
+ } elseif($totalrescnt < $this->limit && !$this->from && !$this->until){
+ # Case 2: not sane, but salvageable.
+ $totalcnt = $rescnt;
+ } else {
+ # Case 3: hopeless. Don't give a total count at all.
+ return wfMsgExt("category-$type-count-limited", 'parse',
+ $wgLang->formatNum( $rescnt ) );
+ }
+ return wfMsgExt( "category-$type-count", 'parse', $wgLang->formatNum( $rescnt ),
+ $wgLang->formatNum( $totalcnt ) );
+ }
+}
diff --git a/includes/Categoryfinder.php b/includes/Categoryfinder.php
index b94dcf5e..d28f2eeb 100644
--- a/includes/Categoryfinder.php
+++ b/includes/Categoryfinder.php
@@ -188,5 +188,3 @@ class Categoryfinder {
}
} # END OF CLASS "Categoryfinder"
-
-
diff --git a/includes/ChangesFeed.php b/includes/ChangesFeed.php
new file mode 100644
index 00000000..9bee1790
--- /dev/null
+++ b/includes/ChangesFeed.php
@@ -0,0 +1,129 @@
+<?php
+
+class ChangesFeed {
+
+ public $format, $type, $titleMsg, $descMsg;
+
+ public function __construct( $format, $type ) {
+ $this->format = $format;
+ $this->type = $type;
+ }
+
+ public function getFeedObject( $title, $description ) {
+ global $wgSitename, $wgContLanguageCode, $wgFeedClasses, $wgTitle;
+ $feedTitle = "$wgSitename - {$title} [$wgContLanguageCode]";
+
+ 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;
+
+ if ( !FeedUtils::checkFeedOutput( $this->format ) ) {
+ return;
+ }
+
+ $timekey = wfMemcKey( $this->type, $this->format, 'timestamp' );
+ $key = wfMemcKey( $this->type, $this->format, 'limit', $limit, 'minor', $hideminor );
+
+ FeedUtils::checkPurge($timekey, $key);
+
+ /*
+ * Bumping around loading up diffs can be pretty slow, so where
+ * possible we want to cache the feed output so the next visitor
+ * gets it quick too.
+ */
+ $cachedFeed = $this->loadFromCache( $lastmod, $timekey, $key );
+ if( is_string( $cachedFeed ) ) {
+ wfDebug( "RC: Outputting cached feed\n" );
+ $feed->httpHeaders();
+ echo $cachedFeed;
+ } else {
+ wfDebug( "RC: rendering new feed and caching it\n" );
+ ob_start();
+ self::generateFeed( $rows, $feed );
+ $cachedFeed = ob_get_contents();
+ ob_end_flush();
+ $this->saveToCache( $cachedFeed, $timekey, $key );
+ }
+ return true;
+ }
+
+ public function saveToCache( $feed, $timekey, $key ) {
+ global $messageMemc;
+ $expire = 3600 * 24; # One day
+ $messageMemc->set( $key, $feed );
+ $messageMemc->set( $timekey, wfTimestamp( TS_MW ), $expire );
+ }
+
+ public function loadFromCache( $lastmod, $timekey, $key ) {
+ global $wgFeedCacheTimeout, $messageMemc;
+ $feedLastmod = $messageMemc->get( $timekey );
+
+ if( ( $wgFeedCacheTimeout > 0 ) && $feedLastmod ) {
+ /*
+ * If the cached feed was rendered very recently, we may
+ * go ahead and use it even if there have been edits made
+ * since it was rendered. This keeps a swarm of requests
+ * from being too bad on a super-frequently edited wiki.
+ */
+
+ $feedAge = time() - wfTimestamp( TS_UNIX, $feedLastmod );
+ $feedLastmodUnix = wfTimestamp( TS_UNIX, $feedLastmod );
+ $lastmodUnix = wfTimestamp( TS_UNIX, $lastmod );
+
+ if( $feedAge < $wgFeedCacheTimeout || $feedLastmodUnix > $lastmodUnix) {
+ wfDebug( "RC: loading feed from cache ($key; $feedLastmod; $lastmod)...\n" );
+ return $messageMemc->get( $key );
+ } else {
+ wfDebug( "RC: cached feed timestamp check failed ($feedLastmod; $lastmod)\n" );
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @todo document
+ * @param $rows Database resource with recentchanges rows
+ * @param $feed Feed object
+ */
+ public static function generateFeed( $rows, &$feed ) {
+ wfProfileIn( __METHOD__ );
+
+ $feed->outHeader();
+
+ # Merge adjacent edits by one user
+ $sorted = array();
+ $n = 0;
+ foreach( $rows as $obj ) {
+ if( $n > 0 &&
+ $obj->rc_namespace >= 0 &&
+ $obj->rc_cur_id == $sorted[$n-1]->rc_cur_id &&
+ $obj->rc_user_text == $sorted[$n-1]->rc_user_text ) {
+ $sorted[$n-1]->rc_last_oldid = $obj->rc_last_oldid;
+ } else {
+ $sorted[$n] = $obj;
+ $n++;
+ }
+ }
+
+ foreach( $sorted as $obj ) {
+ $title = Title::makeTitle( $obj->rc_namespace, $obj->rc_title );
+ $talkpage = $title->getTalkPage();
+ $item = new FeedItem(
+ $title->getPrefixedText(),
+ FeedUtils::formatDiff( $obj ),
+ $title->getFullURL( 'diff=' . $obj->rc_this_oldid . '&oldid=prev' ),
+ $obj->rc_timestamp,
+ ($obj->rc_deleted & Revision::DELETED_USER) ? wfMsgHtml('rev-deleted-user') : $obj->rc_user_text,
+ $talkpage->getFullURL()
+ );
+ $feed->outItem( $item );
+ }
+ $feed->outFooter();
+ wfProfileOut( __METHOD__ );
+ }
+
+} \ No newline at end of file
diff --git a/includes/ChangesList.php b/includes/ChangesList.php
index 507e88fa..436f006e 100644
--- a/includes/ChangesList.php
+++ b/includes/ChangesList.php
@@ -27,7 +27,10 @@ class ChangesList {
# Called by history lists and recent changes
#
- /** @todo document */
+ /**
+ * Changeslist contructor
+ * @param Skin $skin
+ */
function __construct( &$skin ) {
$this->skin =& $skin;
$this->preCacheMessages();
@@ -54,12 +57,12 @@ class ChangesList {
* As we use the same small set of messages in various methods and that
* they are called often, we call them once and save them in $this->message
*/
- function preCacheMessages() {
+ 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 ) {
- $this->message[$msg] = wfMsgExt( $msg, array( 'escape') );
+ $this->message[$msg] = wfMsgExt( $msg, array( 'escapenoentities' ) );
}
}
}
@@ -67,8 +70,14 @@ class ChangesList {
/**
* Returns the appropriate flags for new page, minor change and patrolling
+ * @param bool $new
+ * @param bool $minor
+ * @param bool $patrolled
+ * @param string $nothing, string to use for empty space
+ * @param bool $bot
+ * @return string
*/
- function recentChangesFlags( $new, $minor, $patrolled, $nothing = '&nbsp;', $bot = false ) {
+ 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>'
@@ -80,8 +89,9 @@ class ChangesList {
/**
* Returns text for the start of the tabular part of RC
+ * @return string
*/
- function beginRecentChangesList() {
+ public function beginRecentChangesList() {
$this->rc_cache = array();
$this->rcMoveIndex = 0;
$this->rcCacheIndex = 0;
@@ -92,8 +102,9 @@ class ChangesList {
/**
* Returns text for the end of RC
+ * @return string
*/
- function endRecentChangesList() {
+ public function endRecentChangesList() {
if( $this->rclistOpen ) {
return "</ul>\n";
} else {
@@ -101,8 +112,7 @@ class ChangesList {
}
}
-
- function insertMove( &$s, $rc ) {
+ protected function insertMove( &$s, $rc ) {
# Diff
$s .= '(' . $this->message['diff'] . ') (';
# Hist
@@ -115,7 +125,7 @@ class ChangesList {
$this->skin->makeKnownLinkObj( $rc->getMovedToTitle(), '' ) );
}
- function insertDateHeader(&$s, $rc_timestamp) {
+ protected function insertDateHeader(&$s, $rc_timestamp) {
global $wgLang;
# Make date header if necessary
@@ -131,15 +141,16 @@ class ChangesList {
}
}
- function insertLog(&$s, $title, $logtype) {
+ protected function insertLog(&$s, $title, $logtype) {
$logname = LogPage::logName( $logtype );
$s .= '(' . $this->skin->makeKnownLinkObj($title, $logname ) . ')';
}
-
- function insertDiffHist(&$s, &$rc, $unpatrolled) {
+ protected function insertDiffHist(&$s, &$rc, $unpatrolled) {
# Diff link
- if( $rc->mAttribs['rc_type'] == RC_NEW || $rc->mAttribs['rc_type'] == RC_LOG ) {
+ if( !$this->userCan($rc,Revision::DELETED_TEXT) ) {
+ $diffLink = $this->message['diff'];
+ } else if( $rc->mAttribs['rc_type'] == RC_NEW || $rc->mAttribs['rc_type'] == RC_LOG ) {
$diffLink = $this->message['diff'];
} else {
$rcidparam = $unpatrolled
@@ -163,14 +174,19 @@ class ChangesList {
$s .= ') . . ';
}
- function insertArticleLink(&$s, &$rc, $unpatrolled, $watched) {
+ protected function insertArticleLink(&$s, &$rc, $unpatrolled, $watched) {
# Article link
# 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']
: '';
- $articlelink = ' '. $this->skin->makeKnownLinkObj( $rc->getTitle(), '', $params );
+ 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 )
$articlelink = "<strong class=\"mw-watched\">{$articlelink}</strong>";
global $wgContLang;
@@ -178,27 +194,50 @@ class ChangesList {
wfRunHooks('ChangesListInsertArticleLink',
array(&$this, &$articlelink, &$s, &$rc, $unpatrolled, $watched));
-
+
$s .= ' '.$articlelink;
}
- 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 ) . ' . . ';
}
/** Insert links to user page, user talk page and eventually a blocking link */
- function insertUserRelatedLinks(&$s, &$rc) {
- $s .= $this->skin->userLink( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] );
- $s .= $this->skin->userToolLinks( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] );
+ protected 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'] );
+ $s .= $this->skin->userToolLinks( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] );
+ }
+ }
+
+ /** 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) ) {
+ $s .= ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>';
+ } else {
+ $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 );
+ }
+ }
}
/** insert a formatted comment */
- function insertComment(&$s, &$rc) {
+ protected function insertComment(&$s, &$rc) {
# Add comment
if( $rc->mAttribs['rc_type'] != RC_MOVE && $rc->mAttribs['rc_type'] != RC_MOVE_OVER_REDIRECT ) {
- $s .= $this->skin->commentBlock( $rc->mAttribs['rc_comment'], $rc->getTitle() );
+ // log 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() );
+ }
}
}
@@ -206,15 +245,15 @@ class ChangesList {
* Check whether to enable recent changes patrol features
* @return bool
*/
- function usePatrol() {
- global $wgUseRCPatrol, $wgUser;
- return( $wgUseRCPatrol && ($wgUser->isAllowed('patrol') || $wgUser->isAllowed('patrolmarks')) );
+ public static function usePatrol() {
+ global $wgUser;
+ return $wgUser->useRCPatrol();
}
/**
* Returns the string which indicates the number of watching users
*/
- function numberofWatchingusers( $count ) {
+ protected function numberofWatchingusers( $count ) {
global $wgLang;
static $cache = array();
if ( $count > 0 ) {
@@ -227,6 +266,36 @@ class ChangesList {
return '';
}
}
+
+ /**
+ * Determine if said field of a revision is hidden
+ * @param RCCacheEntry $rc
+ * @param int $field one of DELETED_* bitfield constants
+ * @return bool
+ */
+ public static function isDeleted( $rc, $field ) {
+ return ($rc->mAttribs['rc_deleted'] & $field) == $field;
+ }
+
+ /**
+ * Determine if the current user is allowed to view a particular
+ * field of this revision, if it's marked as deleted.
+ * @param RCCacheEntry $rc
+ * @param int $field
+ * @return bool
+ */
+ public static function userCan( $rc, $field ) {
+ if( ( $rc->mAttribs['rc_deleted'] & $field ) == $field ) {
+ global $wgUser;
+ $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" );
+ return $wgUser->isAllowed( $permission );
+ } else {
+ return true;
+ }
+ }
}
@@ -237,8 +306,8 @@ class OldChangesList extends ChangesList {
/**
* Format a line using the old system (aka without any javascript).
*/
- function recentChangesLine( &$rc, $watched = false ) {
- global $wgContLang, $wgRCShowChangedSize;
+ public function recentChangesLine( &$rc, $watched = false ) {
+ global $wgContLang, $wgRCShowChangedSize, $wgUser;
$fname = 'ChangesList::recentChangesLineOld';
wfProfileIn( $fname );
@@ -248,31 +317,35 @@ class OldChangesList extends ChangesList {
extract( $rc->mAttribs );
# Should patrol-related stuff be shown?
- $unpatrolled = $this->usePatrol() && $rc_patrolled == 0;
+ $unpatrolled = $wgUser->useRCPatrol() && $rc_patrolled == 0;
$this->insertDateHeader($s,$rc_timestamp);
$s .= '<li>';
- // moved pages
+ // Moved pages
if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) {
$this->insertMove( $s, $rc );
- // log entries
- } elseif ( $rc_namespace == NS_SPECIAL ) {
+ // Log entries
+ } elseif( $rc_log_type ) {
+ $logtitle = Title::newFromText( "Log/$rc_log_type", NS_SPECIAL );
+ $this->insertLog( $s, $logtitle, $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" );
}
- // all other stuff
+ // Regular entries
} else {
wfProfileIn($fname.'-page');
$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 );
+ $s .= $this->recentChangesFlags( $rc_type == RC_NEW, $rc_minor, $unpatrolled, '', $rc_bot );
$this->insertArticleLink($s, $rc, $unpatrolled, $watched);
wfProfileOut($fname.'-page');
@@ -285,11 +358,19 @@ class OldChangesList extends ChangesList {
if( $wgRCShowChangedSize ) {
$s .= ( $rc->getCharacterDifference() == '' ? '' : $rc->getCharacterDifference() . ' . . ' );
}
-
+ # User tool links
$this->insertUserRelatedLinks($s,$rc);
+ # Log action text (if any)
+ $this->insertAction($s, $rc);
+ # Edit or log comment
$this->insertComment($s, $rc);
- $s .= rtrim(' ' . $this->numberofWatchingusers($rc->numberofWatchingusers));
+ # Mark revision as deleted if so
+ if ( !$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));
+ }
$s .= "</li>\n";
@@ -308,8 +389,8 @@ class EnhancedChangesList extends ChangesList {
/**
* Format a line for enhanced recentchange (aka with javascript and block of lines).
*/
- function recentChangesLine( &$baseRC, $watched = false ) {
- global $wgLang, $wgContLang;
+ public function recentChangesLine( &$baseRC, $watched = false ) {
+ global $wgLang, $wgContLang, $wgUser;
# Create a specialised object
$rc = RCCacheEntry::newFromParent( $baseRC );
@@ -331,17 +412,20 @@ class EnhancedChangesList extends ChangesList {
}
# Should patrol-related stuff be shown?
- if( $this->usePatrol() ) {
+ if( $wgUser->useRCPatrol() ) {
$rc->unpatrolled = !$rc_patrolled;
} else {
$rc->unpatrolled = false;
}
+ $showdifflinks = true;
# Make article link
+ // Page moves
if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) {
$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' ) {
@@ -352,13 +436,28 @@ class EnhancedChangesList extends ChangesList {
wfDebug( "Unexpected special page in recentchanges\n" );
$clink = '';
}
- } elseif( $rc->unpatrolled && $rc_type == RC_NEW ) {
- # Unpatrolled new page, give rc_id in query
+ // New unpatrolled pages
+ } else if( $rc->unpatrolled && $rc_type == RC_NEW ) {
$clink = $this->skin->makeKnownLinkObj( $rc->getTitle(), '', "rcid={$rc_id}" );
+ // Log entries
+ } 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) ) . ')';
+ } else {
+ $clink = $this->skin->makeLinkObj( $rc->getTitle(), '' );
+ }
+ $watched = false;
+ // Edits
} else {
$clink = $this->skin->makeKnownLinkObj( $rc->getTitle(), '' );
}
+ # Don't show unusable diff links
+ if ( !ChangesList::userCan($rc,Revision::DELETED_TEXT) ) {
+ $showdifflinks = false;
+ }
+
$time = $wgContLang->time( $rc_timestamp, true, true );
$rc->watched = $watched;
$rc->link = $clink;
@@ -375,7 +474,12 @@ class EnhancedChangesList extends ChangesList {
$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 );
- if( $rc_type == RC_NEW || $rc_type == RC_LOG || $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) {
+
+ # Make "diff" an "cur" links
+ if( !$showdifflinks ) {
+ $curLink = $this->message['cur'];
+ $diffLink = $this->message['diff'];
+ } else if( $rc_type == RC_NEW || $rc_type == RC_LOG || $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) {
if( $rc_type != RC_NEW ) {
$curLink = $this->message['cur'];
}
@@ -385,21 +489,27 @@ class EnhancedChangesList extends ChangesList {
}
# Make "last" link
- if( $rc_last_oldid == 0 || $rc_type == RC_LOG || $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) {
+ if( !$showdifflinks ) {
+ $lastLink = $this->message['last'];
+ } else if( $rc_last_oldid == 0 || $rc_type == RC_LOG || $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) {
$lastLink = $this->message['last'];
} else {
$lastLink = $this->skin->makeKnownLinkObj( $rc->getTitle(), $this->message['last'],
- $curIdEq.'&diff='.$rc_this_oldid.'&oldid='.$rc_last_oldid . $rcIdQuery );
+ $curIdEq.'&diff='.$rc_this_oldid.'&oldid='.$rc_last_oldid . $rcIdQuery );
}
- $rc->userlink = $this->skin->userLink( $rc_user, $rc_user_text );
+ # Make user links
+ if( $this->isDeleted($rc,Revision::DELETED_USER) ) {
+ $rc->userlink = ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-user') . '</span>';
+ } else {
+ $rc->userlink = $this->skin->userLink( $rc_user, $rc_user_text );
+ $rc->usertalklink = $this->skin->userToolLinks( $rc_user, $rc_user_text );
+ }
$rc->lastlink = $lastLink;
$rc->curlink = $curLink;
$rc->difflink = $diffLink;
- $rc->usertalklink = $this->skin->userToolLinks( $rc_user, $rc_user_text );
-
# Put accumulated information into the cache, for later display
# Page moves go on their own line
$title = $rc->getTitle();
@@ -408,7 +518,11 @@ class EnhancedChangesList extends ChangesList {
# Use an @ character to prevent collision with page names
$this->rc_cache['@@' . ($this->rcMoveIndex++)] = array($rc);
} else {
- if( !isset ( $this->rc_cache[$secureName] ) ) {
+ # Logs are grouped by type
+ if( $rc_type == RC_LOG ){
+ $secureName = SpecialPage::getTitleFor( 'Log', $rc_log_type )->getPrefixedDBkey();
+ }
+ if( !isset( $this->rc_cache[$secureName] ) ) {
$this->rc_cache[$secureName] = array();
}
array_push( $this->rc_cache[$secureName], $rc );
@@ -419,19 +533,29 @@ class EnhancedChangesList extends ChangesList {
/**
* Enhanced RC group
*/
- function recentChangesBlockGroup( $block ) {
+ protected function recentChangesBlockGroup( $block ) {
global $wgLang, $wgContLang, $wgRCShowChangedSize;
- $r = '';
+ $r = '<table cellpadding="0" cellspacing="0" border="0" style="background: none"><tr>';
# Collate list of users
- $isnew = false;
- $unpatrolled = false;
$userlinks = array();
+ # Other properties
+ $unpatrolled = false;
+ $isnew = false;
+ $curId = $currentRevision = 0;
+ # Some catalyst variables...
+ $namehidden = true;
+ $alllogs = true;
foreach( $block as $rcObj ) {
$oldid = $rcObj->mAttribs['rc_last_oldid'];
if( $rcObj->mAttribs['rc_new'] ) {
$isnew = true;
}
+ // If all log actions to this page were hidden, then don't
+ // give the name of the affected page for this block!
+ if( !$this->isDeleted( $rcObj, LogPage::DELETED_ACTION ) ) {
+ $namehidden = false;
+ }
$u = $rcObj->userlink;
if( !isset( $userlinks[$u] ) ) {
$userlinks[$u] = 0;
@@ -439,6 +563,18 @@ class EnhancedChangesList extends ChangesList {
if( $rcObj->unpatrolled ) {
$unpatrolled = true;
}
+ if( $rcObj->mAttribs['rc_type'] != RC_LOG ) {
+ $alllogs = false;
+ }
+ # Get the latest entry with a page_id and oldid
+ # since logs may not have these.
+ if( !$curId && $rcObj->mAttribs['rc_cur_id'] ) {
+ $curId = $rcObj->mAttribs['rc_cur_id'];
+ }
+ if( !$currentRevision && $rcObj->mAttribs['rc_this_oldid'] ) {
+ $currentRevision = $rcObj->mAttribs['rc_this_oldid'];
+ }
+
$bot = $rcObj->mAttribs['rc_bot'];
$userlinks[$u]++;
}
@@ -465,111 +601,148 @@ class EnhancedChangesList extends ChangesList {
$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>';
- $r .= $tl;
+ $r .= '<td valign="top" style="white-space: nowrap"><tt>'.$tl.'&nbsp;';
# Main line
- $r .= '<tt>';
$r .= $this->recentChangesFlags( $isnew, false, $unpatrolled, '&nbsp;', $bot );
# Timestamp
- $r .= ' '.$block[0]->timestamp.' </tt>';
+ $r .= '&nbsp;'.$block[0]->timestamp.'&nbsp;</tt></td><td>';
# Article link
- $r .= $this->maybeWatchedLink( $block[0]->link, $block[0]->watched );
- $r .= $wgContLang->getDirMark();
-
- $curIdEq = 'curid=' . $block[0]->mAttribs['rc_cur_id'];
- $currentRevision = $block[0]->mAttribs['rc_this_oldid'];
- if( $block[0]->mAttribs['rc_type'] != RC_LOG ) {
- # Changes
-
- $n = count($block);
- static $nchanges = array();
- if ( !isset( $nchanges[$n] ) ) {
- $nchanges[$n] = wfMsgExt( 'nchanges', array( 'parsemag', 'escape'),
- $wgLang->formatNum( $n ) );
- }
+ if( $namehidden ) {
+ $r .= ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>';
+ } else {
+ $r .= $this->maybeWatchedLink( $block[0]->link, $block[0]->watched );
+ }
- $r .= ' (';
+ $r .= $wgContLang->getDirMark();
- if( $isnew ) {
+ $curIdEq = 'curid=' . $curId;
+ # Changes message
+ $n = count($block);
+ static $nchanges = array();
+ if ( !isset( $nchanges[$n] ) ) {
+ $nchanges[$n] = wfMsgExt( 'nchanges', array( 'parsemag', 'escape' ), $wgLang->formatNum( $n ) );
+ }
+ # Total change link
+ $r .= ' ';
+ if( !$alllogs ) {
+ $r .= '(';
+ if( !ChangesList::userCan($rcObj,Revision::DELETED_TEXT) ) {
+ $r .= $nchanges[$n];
+ } else if( $isnew ) {
$r .= $nchanges[$n];
} else {
$r .= $this->skin->makeKnownLinkObj( $block[0]->getTitle(),
$nchanges[$n], $curIdEq."&diff=$currentRevision&oldid=$oldid" );
}
-
$r .= ') . . ';
+ }
- if( $wgRCShowChangedSize ) {
- # Character difference
- $chardiff = $rcObj->getCharacterDifference( $block[ count( $block ) - 1 ]->mAttribs['rc_old_len'],
- $block[0]->mAttribs['rc_new_len'] );
- if( $chardiff == '' ) {
- $r .= ' (';
- } else {
- $r .= ' ' . $chardiff. ' . . ';
- }
- }
-
- # History
+ # Character difference (does not apply if only log items)
+ if( $wgRCShowChangedSize && !$alllogs ) {
+ $last = 0;
+ $first = count($block) - 1;
+ # Some events (like logs) have an "empty" size, so we need to skip those...
+ while( $last < $first && $block[$last]->mAttribs['rc_new_len'] === NULL ) {
+ $last++;
+ }
+ while( $first > $last && $block[$first]->mAttribs['rc_old_len'] === NULL ) {
+ $first--;
+ }
+ # Get net change
+ $chardiff = $rcObj->getCharacterDifference( $block[$first]->mAttribs['rc_old_len'],
+ $block[$last]->mAttribs['rc_new_len'] );
+
+ if( $chardiff == '' ) {
+ $r .= ' ';
+ } else {
+ $r .= ' ' . $chardiff. ' . . ';
+ }
+ }
+
+ # 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 .= ')';
+ $this->message['history'], $curIdEq.'&action=history' ) . ')';
}
$r .= $users;
-
$r .= $this->numberofWatchingusers($block[0]->numberofWatchingusers);
- $r .= "<br />\n";
+
+ $r .= "</td></tr></table>\n";
# Sub-entries
- $r .= '<div id="'.$rci.'" style="display:none">';
+ $r .= '<div id="'.$rci.'" style="display:none;"><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( $rcObj->mAttribs );
- $r .= $this->spacerArrow();
- $r .= '<tt>&nbsp; &nbsp; &nbsp; &nbsp;';
+ #$r .= '<tr><td valign="top">'.$this->spacerArrow();
+ $r .= '<tr><td valign="top">';
+ $r .= '<tt>'.$this->spacerIndent() . $this->spacerIndent();
$r .= $this->recentChangesFlags( $rc_new, $rc_minor, $rcObj->unpatrolled, '&nbsp;', $rc_bot );
- $r .= '&nbsp;</tt>';
+ $r .= '&nbsp;</tt></td><td valign="top">';
$o = '';
if( $rc_this_oldid != 0 ) {
$o = 'oldid='.$rc_this_oldid;
}
+ # Log timestamp
if( $rc_type == RC_LOG ) {
- $link = $rcObj->timestamp;
+ $link = '<tt>'.$rcObj->timestamp.'</tt> ';
+ # Revision link
+ } else if( !ChangesList::userCan($rcObj,Revision::DELETED_TEXT) ) {
+ $link = '<span class="history-deleted"><tt>'.$rcObj->timestamp.'</tt></span> ';
} else {
- $link = $this->skin->makeKnownLinkObj( $rcObj->getTitle(), $rcObj->timestamp, $curIdEq.'&'.$o );
+ $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> ';
}
- $link = '<tt>'.$link.'</tt>';
-
$r .= $link;
- $r .= ' (';
- $r .= $rcObj->curlink;
- $r .= $this->message['semicolon-separator'] . ' ';
- $r .= $rcObj->lastlink;
- $r .= ') . . ';
+
+ if ( !$rc_type == RC_LOG || $rc_type == RC_NEW ) {
+ $r .= ' (';
+ $r .= $rcObj->curlink;
+ $r .= $this->message['semicolon-separator'] . ' ';
+ $r .= $rcObj->lastlink;
+ $r .= ')';
+ }
+ $r .= ' . . ';
# Character diff
if( $wgRCShowChangedSize ) {
$r .= ( $rcObj->getCharacterDifference() == '' ? '' : $rcObj->getCharacterDifference() . ' . . ' ) ;
}
-
+ # User links
$r .= $rcObj->userlink;
$r .= $rcObj->usertalklink;
- $r .= $this->skin->commentBlock( $rc_comment, $rcObj->getTitle() );
- $r .= "<br />\n";
+ // log action
+ parent::insertAction( $r, $rcObj );
+ // log comment
+ parent::insertComment( $r, $rcObj );
+ # Mark revision as deleted
+ if( !$rc_log_type && $this->isDeleted($rcObj,Revision::DELETED_TEXT) ) {
+ $r .= ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
+ }
+
+ $r .= "</td></tr>\n";
}
- $r .= "</div>\n";
+ $r .= "</table></div>\n";
$this->rcCacheIndex++;
return $r;
}
- function maybeWatchedLink( $link, $watched=false ) {
+ protected function maybeWatchedLink( $link, $watched=false ) {
if( $watched ) {
// FIXME: css style might be more appropriate
return '<strong class="mw-watched">' . $link . '</strong>';
@@ -583,9 +756,8 @@ class EnhancedChangesList extends ChangesList {
* @param string $dir one of '', 'd', 'l', 'r'
* @param string $alt text
* @return string HTML <img> tag
- * @access private
*/
- function arrow( $dir, $alt='' ) {
+ protected function arrow( $dir, $alt='' ) {
global $wgStylePath;
$encUrl = htmlspecialchars( $wgStylePath . '/common/images/Arr_' . $dir . '.png' );
$encAlt = htmlspecialchars( $alt );
@@ -596,9 +768,8 @@ class EnhancedChangesList extends ChangesList {
* Generate HTML for a right- or left-facing arrow,
* depending on language direction.
* @return string HTML <img> tag
- * @access private
*/
- function sideArrow() {
+ protected function sideArrow() {
global $wgContLang;
$dir = $wgContLang->isRTL() ? 'l' : 'r';
return $this->arrow( $dir, '+' );
@@ -608,26 +779,32 @@ class EnhancedChangesList extends ChangesList {
* Generate HTML for a down-facing arrow
* depending on language direction.
* @return string HTML <img> tag
- * @access private
*/
- function downArrow() {
+ protected function downArrow() {
return $this->arrow( 'd', '-' );
}
/**
* Generate HTML for a spacer image
* @return string HTML <img> tag
- * @access private
*/
- function spacerArrow() {
+ protected function spacerArrow() {
return $this->arrow( '', ' ' );
}
/**
+ * Add a set of spaces
+ * @return string HTML <td> tag
+ */
+ protected function spacerIndent() {
+ return '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;';
+ }
+
+ /**
* Enhanced RC ungrouped line.
* @return string a HTML formated line (generated using $r)
*/
- function recentChangesBlockLine( $rcObj ) {
+ protected function recentChangesBlockLine( $rcObj ) {
global $wgContLang, $wgRCShowChangedSize;
# Get rc_xxxx variables
@@ -635,29 +812,35 @@ class EnhancedChangesList extends ChangesList {
extract( $rcObj->mAttribs );
$curIdEq = 'curid='.$rc_cur_id;
- $r = '';
+ $r = '<table cellspacing="0" cellpadding="0" border="0" style="background: none"><tr>';
- # Spacer image
- $r .= $this->spacerArrow();
+ $r .= '<td valign="top" style="white-space: nowrap"><tt>' . $this->spacerArrow() . '&nbsp;';
# Flag and Timestamp
- $r .= '<tt>';
-
if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) {
- $r .= '&nbsp;&nbsp;&nbsp;';
+ $r .= '&nbsp;&nbsp;&nbsp;&nbsp;'; // 4 flags -> 4 spaces
} else {
$r .= $this->recentChangesFlags( $rc_type == RC_NEW, $rc_minor, $rcObj->unpatrolled, '&nbsp;', $rc_bot );
}
- $r .= ' '.$rcObj->timestamp.' </tt>';
-
- # Article link
- $r .= $this->maybeWatchedLink( $rcObj->link, $rcObj->watched );
-
- # Diff
- $r .= ' ('. $rcObj->difflink . $this->message['semicolon-separator'] . ' ';
+ $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 );
+ }
- # Hist
- $r .= $this->skin->makeKnownLinkObj( $rcObj->getTitle(), wfMsg( 'hist' ), $curIdEq.'&action=history' ) . ') . . ';
+ # 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 .= ' . . ';
# Character diff
if( $wgRCShowChangedSize ) {
@@ -665,16 +848,32 @@ class EnhancedChangesList extends ChangesList {
}
# User/talk
- $r .= $rcObj->userlink . $rcObj->usertalklink;
+ $r .= ' '.$rcObj->userlink . $rcObj->usertalklink;
- # Comment
+ # Log action (if any)
+ if( $rc_log_type ) {
+ if( $this->isDeleted($rcObj,LogPage::DELETED_ACTION) ) {
+ $r .= ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>';
+ } else {
+ $r .= ' ' . LogPage::actionText( $rc_log_type, $rc_log_action, $rcObj->getTitle(),
+ $this->skin, LogPage::extractParams($rc_params), true, true );
+ }
+ }
+
+ # Edit or log comment
if( $rc_type != RC_MOVE && $rc_type != RC_MOVE_OVER_REDIRECT ) {
- $r .= $this->skin->commentBlock( $rc_comment, $rcObj->getTitle() );
+ // log comment
+ if ( $this->isDeleted($rcObj,LogPage::DELETED_COMMENT) ) {
+ $r .= ' <span class="history-deleted">' . wfMsg('rev-deleted-comment') . '</span>';
+ } else {
+ $r .= $this->skin->commentBlock( $rc_comment, $rcObj->getTitle() );
+ }
}
+ # Show how many people are watching this if enabled
$r .= $this->numberofWatchingusers($rcObj->numberofWatchingusers);
- $r .= "<br />\n";
+ $r .= "</td></tr></table>\n";
return $r;
}
@@ -682,7 +881,7 @@ class EnhancedChangesList extends ChangesList {
* If enhanced RC is in use, this function takes the previously cached
* RC lines, arranges them, and outputs the HTML
*/
- function recentChangesBlock() {
+ protected function recentChangesBlock() {
if( count ( $this->rc_cache ) == 0 ) {
return '';
}
@@ -702,7 +901,7 @@ class EnhancedChangesList extends ChangesList {
* Returns text for the end of RC
* If enhanced RC is in use, returns pretty much all the text
*/
- function endRecentChangesList() {
+ public function endRecentChangesList() {
return $this->recentChangesBlock() . parent::endRecentChangesList();
}
diff --git a/includes/Credits.php b/includes/Credits.php
index 580a8d92..6326e3a2 100644
--- a/includes/Credits.php
+++ b/includes/Credits.php
@@ -29,7 +29,7 @@ function showCreditsPage($article) {
$fname = 'showCreditsPage';
wfProfileIn( $fname );
-
+
$wgOut->setPageTitle( $article->mTitle->getPrefixedText() );
$wgOut->setSubtitle( wfMsg( 'creditspage' ) );
$wgOut->setArticleFlag( false );
@@ -184,5 +184,3 @@ function creditOthersLink($article) {
$skin = $wgUser->getSkin();
return $skin->makeKnownLink($article->mTitle->getPrefixedText(), wfMsg('others'), 'action=credits');
}
-
-
diff --git a/includes/DatabaseFunctions.php b/includes/DatabaseFunctions.php
index a4a0444f..ad6e7f6c 100644
--- a/includes/DatabaseFunctions.php
+++ b/includes/DatabaseFunctions.php
@@ -2,7 +2,8 @@
/**
* Legacy database functions, for compatibility with pre-1.3 code
* NOTE: this file is no longer loaded by default.
- *
+ * @file
+ * @ingroup Database
*/
/**
@@ -399,4 +400,3 @@ function wfUseIndexClause( $index, $dbi = DB_SLAVE ) {
return false;
}
}
-
diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php
index 376e55b1..d6db7030 100644
--- a/includes/DefaultSettings.php
+++ b/includes/DefaultSettings.php
@@ -31,20 +31,25 @@ require_once( "$IP/includes/SiteConfiguration.php" );
$wgConf = new SiteConfiguration;
/** MediaWiki version number */
-$wgVersion = '1.12.0';
+$wgVersion = '1.13.0';
/** Name of the site. It must be changed in LocalSettings.php */
$wgSitename = 'MediaWiki';
-/**
- * Name of the project namespace. If left set to false, $wgSitename will be
+/**
+ * Name of the project namespace. If left set to false, $wgSitename will be
* used instead.
*/
$wgMetaNamespace = false;
/**
- * Name of the project talk namespace. If left set to false, a name derived
- * from the name of the project namespace will be used.
+ * Name of the project talk namespace.
+ *
+ * Normally you can ignore this and it will be something like
+ * $wgMetaNamespace . "_talk". In some languages, you may want to set this
+ * manually for grammatical reasons. It is currently only respected by those
+ * languages where it might be relevant and where no automatic grammar converter
+ * exists.
*/
$wgMetaNamespaceTalk = false;
@@ -90,25 +95,20 @@ if( isset( $_SERVER['SERVER_PORT'] )
$wgScriptPath = '/wiki';
/**
- * Whether to support URLs like index.php/Page_title
- * These often break when PHP is set up in CGI mode.
- * PATH_INFO *may* be correct if cgi.fix_pathinfo is
- * set, but then again it may not; lighttpd converts
- * incoming path data to lowercase on systems with
- * case-insensitive filesystems, and there have been
- * reports of problems on Apache as well.
+ * Whether to support URLs like index.php/Page_title These often break when PHP
+ * is set up in CGI mode. PATH_INFO *may* be correct if cgi.fix_pathinfo is set,
+ * but then again it may not; lighttpd converts incoming path data to lowercase
+ * on systems with case-insensitive filesystems, and there have been reports of
+ * problems on Apache as well.
*
* To be safe we'll continue to keep it off by default.
*
- * Override this to false if $_SERVER['PATH_INFO']
- * contains unexpectedly incorrect garbage, or to
- * true if it is really correct.
- *
- * The default $wgArticlePath will be set based on
- * this value at runtime, but if you have customized
- * it, having this incorrectly set to true can
- * cause redirect loops when "pretty URLs" are used.
+ * Override this to false if $_SERVER['PATH_INFO'] contains unexpectedly
+ * incorrect garbage, or to true if it is really correct.
*
+ * The default $wgArticlePath will be set based on this value at runtime, but if
+ * you have customized it, having this incorrectly set to true can cause
+ * redirect loops when "pretty URLs" are used.
*/
$wgUsePathInfo =
( strpos( php_sapi_name(), 'cgi' ) === false ) &&
@@ -116,53 +116,56 @@ $wgUsePathInfo =
( strpos( php_sapi_name(), 'isapi' ) === false );
-/**#@+
+/**@{
* Script users will request to get articles
- * ATTN: Old installations used wiki.phtml and redirect.phtml -
- * make sure that LocalSettings.php is correctly set!
+ * ATTN: Old installations used wiki.phtml and redirect.phtml - make sure that
+ * LocalSettings.php is correctly set!
*
- * Will be set based on $wgScriptPath in Setup.php if not overridden
- * in LocalSettings.php. Generally you should not need to change this
- * unless you don't like seeing "index.php".
+ * Will be set based on $wgScriptPath in Setup.php if not overridden in
+ * LocalSettings.php. Generally you should not need to change this unless you
+ * don't like seeing "index.php".
*/
-$wgScriptExtension = '.php'; /// extension to append to script names by default
-$wgScript = false; /// defaults to "{$wgScriptPath}/index{$wgScriptExtension}"
-$wgRedirectScript = false; /// defaults to "{$wgScriptPath}/redirect{$wgScriptExtension}"
-/**#@-*/
+$wgScriptExtension = '.php'; ///< extension to append to script names by default
+$wgScript = false; ///< defaults to "{$wgScriptPath}/index{$wgScriptExtension}"
+$wgRedirectScript = false; ///< defaults to "{$wgScriptPath}/redirect{$wgScriptExtension}"
+/**@}*/
-/**#@+
+/**@{
* These various web and file path variables are set to their defaults
* in Setup.php if they are not explicitly set from LocalSettings.php.
* If you do override them, be sure to set them all!
*
* These will relatively rarely need to be set manually, unless you are
* splitting style sheets or images outside the main document root.
- *
- * @global string
*/
/**
* style path as seen by users
*/
-$wgStylePath = false; /// defaults to "{$wgScriptPath}/skins"
+$wgStylePath = false; ///< defaults to "{$wgScriptPath}/skins"
/**
* filesystem stylesheets directory
*/
-$wgStyleDirectory = false; /// defaults to "{$IP}/skins"
+$wgStyleDirectory = false; ///< defaults to "{$IP}/skins"
$wgStyleSheetPath = &$wgStylePath;
-$wgArticlePath = false; /// default to "{$wgScript}/$1" or "{$wgScript}?title=$1", depending on $wgUsePathInfo
+$wgArticlePath = false; ///< default to "{$wgScript}/$1" or "{$wgScript}?title=$1", depending on $wgUsePathInfo
$wgVariantArticlePath = false;
-$wgUploadPath = false; /// defaults to "{$wgScriptPath}/images"
-$wgUploadDirectory = false; /// defaults to "{$IP}/images"
+$wgUploadPath = false; ///< defaults to "{$wgScriptPath}/images"
+$wgUploadDirectory = false; ///< defaults to "{$IP}/images"
$wgHashedUploadDirectory = true;
-$wgLogo = false; /// defaults to "{$wgStylePath}/common/images/wiki.png"
+$wgLogo = false; ///< defaults to "{$wgStylePath}/common/images/wiki.png"
$wgFavicon = '/favicon.ico';
-$wgAppleTouchIcon = false; /// This one'll actually default to off. For iPhone and iPod Touch web app bookmarks
-$wgMathPath = false; /// defaults to "{$wgUploadPath}/math"
-$wgMathDirectory = false; /// defaults to "{$wgUploadDirectory}/math"
-$wgTmpDirectory = false; /// defaults to "{$wgUploadDirectory}/tmp"
+$wgAppleTouchIcon = false; ///< This one'll actually default to off. For iPhone and iPod Touch web app bookmarks
+$wgMathPath = false; ///< defaults to "{$wgUploadPath}/math"
+$wgMathDirectory = false; ///< defaults to "{$wgUploadDirectory}/math"
+$wgTmpDirectory = false; ///< defaults to "{$wgUploadDirectory}/tmp"
$wgUploadBaseUrl = "";
-/**#@-*/
+/**@}*/
+
+/**
+ * Default value for chmoding of new directories.
+ */
+$wgDirectoryMode = 0777;
/**
* New file storage paths; currently used only for deleted files.
@@ -172,46 +175,46 @@ $wgUploadBaseUrl = "";
*
*/
$wgFileStore = array();
-$wgFileStore['deleted']['directory'] = false;// Defaults to $wgUploadDirectory/deleted
-$wgFileStore['deleted']['url'] = null; // Private
-$wgFileStore['deleted']['hash'] = 3; // 3-level subdirectory split
+$wgFileStore['deleted']['directory'] = false;///< Defaults to $wgUploadDirectory/deleted
+$wgFileStore['deleted']['url'] = null; ///< Private
+$wgFileStore['deleted']['hash'] = 3; ///< 3-level subdirectory split
-/**#@+
+/**@{
* File repository structures
*
* $wgLocalFileRepo is a single repository structure, and $wgForeignFileRepo is
- * a an array of such structures. Each repository structure is an associative
- * array of properties configuring the repository.
+ * a an array of such structures. Each repository structure is an associative
+ * array of properties configuring the repository.
*
* Properties required for all repos:
- * class The class name for the repository. May come from the core or an extension.
+ * class The class name for the repository. May come from the core or an extension.
* The core repository classes are LocalRepo, ForeignDBRepo, FSRepo.
*
* name A unique name for the repository.
- *
+ *
* For all core repos:
* url Base public URL
* hashLevels The number of directory levels for hash-based division of files
* thumbScriptUrl The URL for thumb.php (optional, not recommended)
- * transformVia404 Whether to skip media file transformation on parse and rely on a 404
+ * transformVia404 Whether to skip media file transformation on parse and rely on a 404
* handler instead.
- * initialCapital Equivalent to $wgCapitalLinks, determines whether filenames implicitly
+ * initialCapital Equivalent to $wgCapitalLinks, determines whether filenames implicitly
* start with a capital letter. The current implementation may give incorrect
- * description page links when the local $wgCapitalLinks and initialCapital
+ * description page links when the local $wgCapitalLinks and initialCapital
* are mismatched.
* pathDisclosureProtection
- * May be 'paranoid' to remove all parameters from error messages, 'none' to
- * leave the paths in unchanged, or 'simple' to replace paths with
+ * May be 'paranoid' to remove all parameters from error messages, 'none' to
+ * leave the paths in unchanged, or 'simple' to replace paths with
* placeholders. Default for LocalRepo is 'simple'.
*
* These settings describe a foreign MediaWiki installation. They are optional, and will be ignored
* for local repositories:
* descBaseUrl URL of image description pages, e.g. http://en.wikipedia.org/wiki/Image:
- * scriptDirUrl URL of the MediaWiki installation, equivalent to $wgScriptPath, e.g.
+ * scriptDirUrl URL of the MediaWiki installation, equivalent to $wgScriptPath, e.g.
* http://en.wikipedia.org/w
*
* articleUrl Equivalent to $wgArticlePath, e.g. http://en.wikipedia.org/wiki/$1
- * fetchDescription Fetch the text of the remote file description page. Equivalent to
+ * fetchDescription Fetch the text of the remote file description page. Equivalent to
* $wgFetchCommonsDescriptions.
*
* ForeignDBRepo:
@@ -220,12 +223,12 @@ $wgFileStore['deleted']['hash'] = 3; // 3-level subdirectory split
* tablePrefix Table prefix, the foreign wiki's $wgDBprefix
* hasSharedCache True if the wiki's shared cache is accessible via the local $wgMemc
*
- * The default is to initialise these arrays from the MW<1.11 backwards compatible settings:
+ * The default is to initialise these arrays from the MW<1.11 backwards compatible settings:
* $wgUploadPath, $wgThumbnailScriptPath, $wgSharedUploadDirectory, etc.
*/
$wgLocalFileRepo = false;
$wgForeignFileRepos = array();
-/**#@-*/
+/**@}*/
/**
* Allowed title characters -- regex character class
@@ -274,7 +277,6 @@ $wgUrlProtocols = array(
/** internal name of virus scanner. This servers as a key to the $wgAntivirusSetup array.
* Set this to NULL to disable virus scanning. If not null, every file uploaded will be scanned for viruses.
- * @global string $wgAntivirus
*/
$wgAntivirus= NULL;
@@ -301,8 +303,6 @@ $wgAntivirus= NULL;
* "messagepattern" is a perl regular expression to extract the meaningful part of the scanners
* output. The relevant part should be matched as group one (\1).
* If not defined or the pattern does not match, the full message is shown to the user.
- *
- * @global array $wgAntivirusSetup
*/
$wgAntivirusSetup = array(
@@ -336,51 +336,54 @@ $wgAntivirusSetup = array(
);
-/** Determines if a failed virus scan (AV_SCAN_FAILED) will cause the file to be rejected.
- * @global boolean $wgAntivirusRequired
-*/
+/** Determines if a failed virus scan (AV_SCAN_FAILED) will cause the file to be rejected. */
$wgAntivirusRequired= true;
-/** Determines if the mime type of uploaded files should be checked
- * @global boolean $wgVerifyMimeType
-*/
+/** Determines if the mime type of uploaded files should be checked */
$wgVerifyMimeType= true;
-/** Sets the mime type definition file to use by MimeMagic.php.
-* @global string $wgMimeTypeFile
-*/
+/** Sets the mime type definition file to use by MimeMagic.php. */
$wgMimeTypeFile= "includes/mime.types";
#$wgMimeTypeFile= "/etc/mime.types";
#$wgMimeTypeFile= NULL; #use built-in defaults only.
-/** Sets the mime type info file to use by MimeMagic.php.
-* @global string $wgMimeInfoFile
-*/
+/** Sets the mime type info file to use by MimeMagic.php. */
$wgMimeInfoFile= "includes/mime.info";
#$wgMimeInfoFile= NULL; #use built-in defaults only.
/** Switch for loading the FileInfo extension by PECL at runtime.
- * This should be used only if fileinfo is installed as a shared object
+ * This should be used only if fileinfo is installed as a shared object
* or a dynamic libary
- * @global string $wgLoadFileinfoExtension
-*/
+ */
$wgLoadFileinfoExtension= false;
/** Sets an external mime detector program. The command must print only
* the mime type to standard output.
* The name of the file to process will be appended to the command given here.
* If not set or NULL, mime_content_type will be used if available.
-*/
+ */
$wgMimeDetectorCommand= NULL; # use internal mime_content_type function, available since php 4.3.0
#$wgMimeDetectorCommand= "file -bi"; #use external mime detector (Linux)
/** Switch for trivial mime detection. Used by thumb.php to disable all fance
* things, because only a few types of images are needed and file extensions
* can be trusted.
-*/
+ */
$wgTrivialMimeDetection= false;
/**
+ * Additional XML types we can allow via mime-detection.
+ * array = ( 'rootElement' => 'associatedMimeType' )
+ */
+$wgXMLMimeTypes = array(
+ 'http://www.w3.org/2000/svg:svg' => 'image/svg+xml',
+ 'svg' => 'image/svg+xml',
+ 'http://www.lysator.liu.se/~alla/dia/:diagram' => 'application/x-dia-diagram',
+ 'http://www.w3.org/1999/xhtml:html' => 'text/html', // application/xhtml+xml?
+ 'html' => 'text/html', // application/xhtml+xml?
+);
+
+/**
* To set 'pretty' URL paths for actions other than
* plain page views, add to this array. For instance:
* 'edit' => "$wgScriptPath/edit/$1"
@@ -430,7 +433,7 @@ $wgMaxUploadSize = 1024*1024*100; # 100MB
* Useful if you want to use a shared repository by default
* without disabling local uploads (use $wgEnableUploads = false for that)
* e.g. $wgUploadNavigationUrl = 'http://commons.wikimedia.org/wiki/Special:Upload';
-*/
+ */
$wgUploadNavigationUrl = false;
/**
@@ -461,12 +464,6 @@ $wgHashedSharedUploadDirectory = true;
*/
$wgRepositoryBaseUrl = "http://commons.wikimedia.org/wiki/Image:";
-/**
- * Experimental feature still under debugging.
- */
-$wgFileRedirects = false;
-
-
#
# Email settings
#
@@ -474,7 +471,6 @@ $wgFileRedirects = false;
/**
* Site admin email address
* Default to wikiadmin@SERVER_NAME
- * @global string $wgEmergencyContact
*/
$wgEmergencyContact = 'wikiadmin@' . $wgServerName;
@@ -482,7 +478,6 @@ $wgEmergencyContact = 'wikiadmin@' . $wgServerName;
* Password reminder email address
* The address we should use as sender when a user is requesting his password
* Default to apache@SERVER_NAME
- * @global string $wgPasswordSender
*/
$wgPasswordSender = 'MediaWiki Mail <apache@' . $wgServerName . '>';
@@ -498,14 +493,12 @@ $wgNoReplyAddress = 'reply@not.possible';
* Set to true to enable the e-mail basic features:
* Password reminders, etc. If sending e-mail on your
* server doesn't work, you might want to disable this.
- * @global bool $wgEnableEmail
*/
$wgEnableEmail = true;
/**
* Set to true to enable user-to-user e-mail.
* This can potentially be abused, as it's hard to track.
- * @global bool $wgEnableUserEmail
*/
$wgEnableUserEmail = true;
@@ -537,13 +530,11 @@ $wgPasswordReminderResendTime = 24;
* "username" => user,
* "password" => password
* </code>
- *
- * @global mixed $wgSMTP
*/
$wgSMTP = false;
-/**#@+
+/**@{
* Database settings
*/
/** database host name or ip address */
@@ -556,21 +547,39 @@ $wgDBname = 'wikidb';
$wgDBconnection = '';
/** Database username */
$wgDBuser = 'wikiuser';
-/** Database type
- */
-$wgDBtype = "mysql";
+/** Database user's password */
+$wgDBpassword = '';
+/** Database type */
+$wgDBtype = 'mysql';
+
/** Search type
* Leave as null to select the default search engine for the
- * selected database type (eg SearchMySQL4), or set to a class
+ * selected database type (eg SearchMySQL), or set to a class
* name to override to a custom search engine.
*/
$wgSearchType = null;
+
/** Table name prefix */
$wgDBprefix = '';
/** MySQL table options to use during installation or update */
-$wgDBTableOptions = 'TYPE=InnoDB';
+$wgDBTableOptions = 'ENGINE=InnoDB';
+
+/** Mediawiki schema */
+$wgDBmwschema = 'mediawiki';
+/** Tsearch2 schema */
+$wgDBts2schema = 'public';
+
+/** To override default SQLite data directory ($docroot/../data) */
+$wgSQLiteDataDir = '';
+
+/**
+ * Make all database connections secretly go to localhost. Fool the load balancer
+ * thinking there is an arbitrarily large cluster of servers to connect to.
+ * Useful for debugging.
+ */
+$wgAllDBsAreLocalhost = false;
-/**#@-*/
+/**@}*/
/** Live high performance sites should disable this - some checks acquire giant mysql locks */
@@ -578,55 +587,76 @@ $wgCheckDBSchema = true;
/**
- * Shared database for multiple wikis. Presently used for storing a user table
+ * Shared database for multiple wikis. Commonly used for storing a user table
* for single sign-on. The server for this database must be the same as for the
* main database.
+ * For backwards compatibility the shared prefix is set to the same as the local
+ * prefix, and the user table is listed in the default list of shared tables.
+ *
+ * $wgSharedTables may be customized with a list of tables to share in the shared
+ * datbase. However it is advised to limit what tables you do share as many of
+ * MediaWiki's tables may have side effects if you try to share them.
* EXPERIMENTAL
*/
-$wgSharedDB = null;
-
-# Database load balancer
-# This is a two-dimensional array, an array of server info structures
-# Fields are:
-# host: Host name
-# dbname: Default database name
-# user: DB user
-# password: DB password
-# type: "mysql" or "postgres"
-# load: ratio of DB_SLAVE load, must be >=0, the sum of all loads must be >0
-# groupLoads: array of load ratios, the key is the query group name. A query may belong
-# to several groups, the most specific group defined here is used.
-#
-# flags: bit field
-# DBO_DEFAULT -- turns on DBO_TRX only if !$wgCommandLineMode (recommended)
-# DBO_DEBUG -- equivalent of $wgDebugDumpSql
-# DBO_TRX -- wrap entire request in a transaction
-# DBO_IGNORE -- ignore errors (not useful in LocalSettings.php)
-# DBO_NOBUFFER -- turn off buffering (not useful in LocalSettings.php)
-#
-# max lag: (optional) Maximum replication lag before a slave will taken out of rotation
-# max threads: (optional) Maximum number of running threads
-#
-# These and any other user-defined properties will be assigned to the mLBInfo member
-# variable of the Database object.
-#
-# Leave at false to use the single-server variables above. If you set this
-# variable, the single-server variables will generally be ignored (except
-# perhaps in some command-line scripts).
-#
-# The first server listed in this array (with key 0) will be the master. The
-# rest of the servers will be slaves. To prevent writes to your slaves due to
-# accidental misconfiguration or MediaWiki bugs, set read_only=1 on all your
-# slaves in my.cnf. You can set read_only mode at runtime using:
-#
-# SET @@read_only=1;
-#
-# Since the effect of writing to a slave is so damaging and difficult to clean
-# up, we at Wikimedia set read_only=1 in my.cnf on all our DB servers, even
-# our masters, and then set read_only=0 on masters at runtime.
-#
+$wgSharedDB = null;
+$wgSharedPrefix = false; # Defaults to $wgDBprefix
+$wgSharedTables = array( 'user' );
+
+/**
+ * Database load balancer
+ * This is a two-dimensional array, an array of server info structures
+ * Fields are:
+ * host: Host name
+ * dbname: Default database name
+ * user: DB user
+ * password: DB password
+ * type: "mysql" or "postgres"
+ * load: ratio of DB_SLAVE load, must be >=0, the sum of all loads must be >0
+ * groupLoads: array of load ratios, the key is the query group name. A query may belong
+ * to several groups, the most specific group defined here is used.
+ *
+ * flags: bit field
+ * DBO_DEFAULT -- turns on DBO_TRX only if !$wgCommandLineMode (recommended)
+ * DBO_DEBUG -- equivalent of $wgDebugDumpSql
+ * DBO_TRX -- wrap entire request in a transaction
+ * DBO_IGNORE -- ignore errors (not useful in LocalSettings.php)
+ * DBO_NOBUFFER -- turn off buffering (not useful in LocalSettings.php)
+ *
+ * max lag: (optional) Maximum replication lag before a slave will taken out of rotation
+ * max threads: (optional) Maximum number of running threads
+ *
+ * These and any other user-defined properties will be assigned to the mLBInfo member
+ * variable of the Database object.
+ *
+ * Leave at false to use the single-server variables above. If you set this
+ * variable, the single-server variables will generally be ignored (except
+ * perhaps in some command-line scripts).
+ *
+ * The first server listed in this array (with key 0) will be the master. The
+ * rest of the servers will be slaves. To prevent writes to your slaves due to
+ * accidental misconfiguration or MediaWiki bugs, set read_only=1 on all your
+ * slaves in my.cnf. You can set read_only mode at runtime using:
+ *
+ * SET @@read_only=1;
+ *
+ * Since the effect of writing to a slave is so damaging and difficult to clean
+ * up, we at Wikimedia set read_only=1 in my.cnf on all our DB servers, even
+ * our masters, and then set read_only=0 on masters at runtime.
+ */
$wgDBservers = false;
+/**
+ * Load balancer factory configuration
+ * To set up a multi-master wiki farm, set the class here to something that
+ * can return a LoadBalancer with an appropriate master on a call to getMainLB().
+ * The class identified here is responsible for reading $wgDBservers,
+ * $wgDBserver, etc., so overriding it may cause those globals to be ignored.
+ *
+ * The LBFactory_Multi class is provided for this purpose, please see
+ * includes/db/LBFactory_Multi.php for configuration information.
+ */
+$wgLBFactoryConf = array( 'class' => 'LBFactory_Simple' );
+
/** How long to wait for a slave to catch up to the master */
$wgMasterWaitTimeout = 10;
@@ -681,40 +711,28 @@ $wgDBmysql5 = false;
*/
$wgLocalDatabases = array();
-/**
- * For multi-wiki clusters with multiple master servers; if an alternate
- * is listed for the requested database, a connection to it will be opened
- * instead of to the current wiki's regular master server when cross-wiki
- * data operations are done from here.
- *
- * Requires that the other server be accessible by network, with the same
- * username/password as the primary.
- *
- * eg $wgAlternateMaster['enwiki'] = 'ariel';
- */
-$wgAlternateMaster = array();
-
-/**
+/** @{
* Object cache settings
* See Defines.php for types
*/
$wgMainCacheType = CACHE_NONE;
$wgMessageCacheType = CACHE_ANYTHING;
$wgParserCacheType = CACHE_ANYTHING;
+/**@}*/
$wgParserCacheExpireTime = 86400;
$wgSessionsInMemcached = false;
-$wgLinkCacheMemcached = false; # Not fully tested
-/**
+/**@{
* Memcached-specific settings
* See docs/memcached.txt
*/
$wgUseMemCached = false;
-$wgMemCachedDebug = false; # Will be set to false in Setup.php, if the server isn't working
+$wgMemCachedDebug = false; ///< Will be set to false in Setup.php, if the server isn't working
$wgMemCachedServers = array( '127.0.0.1:11000' );
$wgMemCachedPersistent = false;
+/**@}*/
/**
* Directory for local copy of message cache, for use in addition to memcached
@@ -759,14 +777,16 @@ $wgInputEncoding = 'UTF-8';
$wgOutputEncoding = 'UTF-8';
$wgEditEncoding = '';
-# Set this to eg 'ISO-8859-1' to perform character set
-# conversion when loading old revisions not marked with
-# "utf-8" flag. Use this when converting wiki to UTF-8
-# without the burdensome mass conversion of old text data.
-#
-# NOTE! This DOES NOT touch any fields other than old_text.
-# Titles, comments, user names, etc still must be converted
-# en masse in the database before continuing as a UTF-8 wiki.
+/**
+ * Set this to eg 'ISO-8859-1' to perform character set
+ * conversion when loading old revisions not marked with
+ * "utf-8" flag. Use this when converting wiki to UTF-8
+ * without the burdensome mass conversion of old text data.
+ *
+ * NOTE! This DOES NOT touch any fields other than old_text.
+ * Titles, comments, user names, etc still must be converted
+ * en masse in the database before continuing as a UTF-8 wiki.
+ */
$wgLegacyEncoding = false;
/**
@@ -792,12 +812,14 @@ $wgDocType = '-//W3C//DTD XHTML 1.0 Transitional//EN';
$wgDTD = 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd';
$wgXhtmlDefaultNamespace = 'http://www.w3.org/1999/xhtml';
-# Permit other namespaces in addition to the w3.org default.
-# Use the prefix for the key and the namespace for the value. For
-# example:
-# $wgXhtmlNamespaces['svg'] = 'http://www.w3.org/2000/svg';
-# Normally we wouldn't have to define this in the root <html>
-# element, but IE needs it there in some circumstances.
+/**
+ * Permit other namespaces in addition to the w3.org default.
+ * Use the prefix for the key and the namespace for the value. For
+ * example:
+ * $wgXhtmlNamespaces['svg'] = 'http://www.w3.org/2000/svg';
+ * Normally we wouldn't have to define this in the root <html>
+ * element, but IE needs it there in some circumstances.
+ */
$wgXhtmlNamespaces = array();
/** Enable to allow rewriting dates in page text.
@@ -836,10 +858,10 @@ $wgMaxMsgCacheEntrySize = 10000;
*/
$wgCheckSerialized = true;
-# Whether to enable language variant conversion.
+/** Whether to enable language variant conversion. */
$wgDisableLangConversion = false;
-# Default variant code, if false, the default will be the language code
+/** Default variant code, if false, the default will be the language code */
$wgDefaultLanguageVariant = false;
/**
@@ -849,21 +871,15 @@ $wgDefaultLanguageVariant = false;
*/
$wgLoginLanguageSelector = false;
-# Whether to use zhdaemon to perform Chinese text processing
-# zhdaemon is under developement, so normally you don't want to
-# use it unless for testing
+/**
+ * Whether to use zhdaemon to perform Chinese text processing
+ * zhdaemon is under developement, so normally you don't want to
+ * use it unless for testing
+ */
$wgUseZhdaemon = false;
$wgZhdaemonHost="localhost";
$wgZhdaemonPort=2004;
-/** Normally you can ignore this and it will be something
- like $wgMetaNamespace . "_talk". In some languages, you
- may want to set this manually for grammatical reasons.
- It is currently only respected by those languages
- where it might be relevant and where no automatic
- grammar converter exists.
-*/
-$wgMetaNamespaceTalk = false;
# Miscellaneous configuration settings
#
@@ -886,7 +902,7 @@ $wgInterwikiExpiry = 10800; # Expiry time for cache of interwiki table
2 - wiki and global levels
3 - site levels
$wgInterwikiFallbackSite - if unable to resolve from cache
-*/
+ */
$wgInterwikiCache = false;
$wgInterwikiScopes = 3;
$wgInterwikiFallbackSite = 'wiki';
@@ -907,19 +923,22 @@ $wgRedirectSources = false;
$wgShowIPinHeader = true; # For non-logged in users
-$wgMaxNameChars = 255; # Maximum number of bytes in username
$wgMaxSigChars = 255; # Maximum number of Unicode characters in signature
$wgMaxArticleSize = 2048; # Maximum article size in kilobytes
+# Maximum number of bytes in username. You want to run the maintenance
+# script ./maintenancecheckUsernames.php once you have changed this value
+$wgMaxNameChars = 255;
$wgMaxPPNodeCount = 1000000; # A complexity limit on template expansion
/**
* Maximum recursion depth for templates within templates.
- * The current parser adds two levels to the PHP call stack for each template,
+ * The current parser adds two levels to the PHP call stack for each template,
* and xdebug limits the call stack to 100 by default. So this should hopefully
* stop the parser before it hits the xdebug limit.
*/
$wgMaxTemplateDepth = 40;
+$wgMaxPPExpandDepth = 40;
$wgExtraSubtitle = '';
$wgSiteSupportPage = ''; # A page where you users can receive donations
@@ -929,16 +948,13 @@ $wgSiteSupportPage = ''; # A page where you users can receive donations
* Its contents will be shown to users as part of the read-only warning
* message.
*/
-$wgReadOnlyFile = false; /// defaults to "{$wgUploadDirectory}/lock_yBgMBwiR";
+$wgReadOnlyFile = false; ///< defaults to "{$wgUploadDirectory}/lock_yBgMBwiR";
/**
* The debug log file should be not be publicly accessible if it is used, as it
* may contain private data. */
$wgDebugLogFile = '';
-/**#@+
- * @global bool
- */
$wgDebugRedirects = false;
$wgDebugRawPage = false; # Avoid overlapping debug entries by leaving out CSS
@@ -960,6 +976,11 @@ $wgDebugDumpSql = false;
$wgDebugLogGroups = array();
/**
+ * Show the contents of $wgHooks in Special:Version
+ */
+$wgSpecialVersionShowHooks = false;
+
+/**
* Whether to show "we're sorry, but there has been a database error" pages.
* Displaying errors aids in debugging, but may display information useful
* to an attacker.
@@ -1026,18 +1047,16 @@ $wgSidebarCacheExpiry = 86400;
*
* Retroactively changing this variable will not affect
* the existing count (cf. maintenance/recount.sql).
-*/
+ */
$wgUseCommaCount = false;
-/**#@-*/
-
/**
* wgHitcounterUpdateFreq sets how often page counters should be updated, higher
* values are easier on the database. A value of 1 causes the counters to be
* updated on every hit, any higher value n cause them to update *on average*
* every n hits. Should be set to either 1 or something largish, eg 1000, for
* maximum efficiency.
-*/
+ */
$wgHitcounterUpdateFreq = 1;
# Basic user rights and block settings
@@ -1048,7 +1067,8 @@ $wgBlockAllowsUTEdit = false; # Blocks allow users to edit their own user tal
$wgSysopEmailBans = true; # Allow sysops to ban users from accessing Emailuser
# Pages anonymous user may see as an array, e.g.:
-# array ( "Main Page", "Special:Userlogin", "Wikipedia:Help");
+# array ( "Main Page", "Wikipedia:Help");
+# Special:Userlogin and Special:Resetpass are always whitelisted.
# NOTE: This will only work if $wgGroupPermissions['*']['read']
# is false -- see below. Otherwise, ALL pages are accessible,
# regardless of this setting.
@@ -1057,7 +1077,7 @@ $wgSysopEmailBans = true; # Allow sysops to ban users from accessing Email
# directory name unguessable, or use .htaccess to protect it.
$wgWhitelistRead = false;
-/**
+/**
* Should editors be required to have a validated e-mail
* address before being allowed to edit?
*/
@@ -1083,79 +1103,88 @@ $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['*' ]['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' ]['read'] = true;
-$wgGroupPermissions['user' ]['edit'] = true;
-$wgGroupPermissions['user' ]['createpage'] = true;
-$wgGroupPermissions['user' ]['createtalk'] = 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' ]['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;
-// Implicit group for accounts with confirmed email addresses
-// This has little use when email address confirmation is off
-$wgGroupPermissions['emailconfirmed']['emailconfirmed'] = 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' ]['bot'] = true;
+$wgGroupPermissions['bot' ]['autoconfirmed'] = true;
+$wgGroupPermissions['bot' ]['nominornewtalk'] = true;
+$wgGroupPermissions['bot' ]['autopatrol'] = true;
$wgGroupPermissions['bot' ]['suppressredirect'] = true;
-$wgGroupPermissions['bot' ]['apihighlimits'] = 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;
-$wgGroupPermissions['sysop']['createaccount'] = true;
-$wgGroupPermissions['sysop']['delete'] = true;
-$wgGroupPermissions['sysop']['bigdelete'] = true; // can be separately configured for pages with > $wgDeleteRevisionsLimit revs
-$wgGroupPermissions['sysop']['deletedhistory'] = true; // can view deleted history entries, but not see or restore the text
-$wgGroupPermissions['sysop']['undelete'] = true;
-$wgGroupPermissions['sysop']['editinterface'] = true;
-$wgGroupPermissions['sysop']['editusercssjs'] = true;
-$wgGroupPermissions['sysop']['import'] = true;
-$wgGroupPermissions['sysop']['importupload'] = true;
-$wgGroupPermissions['sysop']['move'] = true;
-$wgGroupPermissions['sysop']['patrol'] = true;
-$wgGroupPermissions['sysop']['autopatrol'] = true;
-$wgGroupPermissions['sysop']['protect'] = true;
-$wgGroupPermissions['sysop']['proxyunbannable'] = true;
-$wgGroupPermissions['sysop']['rollback'] = true;
-$wgGroupPermissions['sysop']['trackback'] = true;
-$wgGroupPermissions['sysop']['upload'] = true;
-$wgGroupPermissions['sysop']['reupload'] = true;
-$wgGroupPermissions['sysop']['reupload-shared'] = true;
-$wgGroupPermissions['sysop']['unwatchedpages'] = true;
-$wgGroupPermissions['sysop']['autoconfirmed'] = true;
-$wgGroupPermissions['sysop']['upload_by_url'] = true;
-$wgGroupPermissions['sysop']['ipblock-exempt'] = true;
-$wgGroupPermissions['sysop']['blockemail'] = true;
-$wgGroupPermissions['sysop']['markbotedits'] = true;
+$wgGroupPermissions['sysop']['block'] = true;
+$wgGroupPermissions['sysop']['createaccount'] = true;
+$wgGroupPermissions['sysop']['delete'] = true;
+$wgGroupPermissions['sysop']['bigdelete'] = true; // can be separately configured for pages with > $wgDeleteRevisionsLimit revs
+$wgGroupPermissions['sysop']['deletedhistory'] = true; // can view deleted history entries, but not see or restore the text
+$wgGroupPermissions['sysop']['undelete'] = true;
+$wgGroupPermissions['sysop']['editinterface'] = true;
+$wgGroupPermissions['sysop']['editusercssjs'] = true;
+$wgGroupPermissions['sysop']['import'] = true;
+$wgGroupPermissions['sysop']['importupload'] = true;
+$wgGroupPermissions['sysop']['move'] = true;
+$wgGroupPermissions['sysop']['move-subpages'] = true;
+$wgGroupPermissions['sysop']['patrol'] = true;
+$wgGroupPermissions['sysop']['autopatrol'] = true;
+$wgGroupPermissions['sysop']['protect'] = true;
+$wgGroupPermissions['sysop']['proxyunbannable'] = true;
+$wgGroupPermissions['sysop']['rollback'] = true;
+$wgGroupPermissions['sysop']['trackback'] = true;
+$wgGroupPermissions['sysop']['upload'] = true;
+$wgGroupPermissions['sysop']['reupload'] = true;
+$wgGroupPermissions['sysop']['reupload-shared'] = true;
+$wgGroupPermissions['sysop']['unwatchedpages'] = true;
+$wgGroupPermissions['sysop']['autoconfirmed'] = true;
+$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']['mergehistory'] = true;
+$wgGroupPermissions['sysop']['apihighlimits'] = true;
+$wgGroupPermissions['sysop']['browsearchive'] = true;
+$wgGroupPermissions['sysop']['noratelimit'] = true;
+#$wgGroupPermissions['sysop']['mergehistory'] = true;
// Permission to change users' group assignments
-$wgGroupPermissions['bureaucrat']['userrights'] = true;
+$wgGroupPermissions['bureaucrat']['userrights'] = true;
+$wgGroupPermissions['bureaucrat']['noratelimit'] = true;
// Permission to change users' groups assignments across wikis
#$wgGroupPermissions['bureaucrat']['userrights-interwiki'] = true;
-// Experimental permissions, not ready for production use
-//$wgGroupPermissions['sysop']['deleterevision'] = true;
-//$wgGroupPermissions['bureaucrat']['hiderevision'] = true;
+#$wgGroupPermissions['sysop']['deleterevision'] = true;
+// To hide usernames from users and Sysops
+#$wgGroupPermissions['suppress']['hideuser'] = true;
+// To hide revisions/log items from users and Sysops
+#$wgGroupPermissions['suppress']['suppressrevision'] = true;
+// For private suppression log access
+#$wgGroupPermissions['suppress']['suppressionlog'] = true;
/**
* The developer group is deprecated, but can be activated if need be
@@ -1169,7 +1198,7 @@ $wgGroupPermissions['bureaucrat']['userrights'] = true;
/**
* Implicit groups, aren't shown on Special:Listusers or somewhere else
*/
-$wgImplicitGroups = array( '*', 'user', 'autoconfirmed', 'emailconfirmed' );
+$wgImplicitGroups = array( '*', 'user', 'autoconfirmed' );
/**
* These are the groups that users are allowed to add to or remove from
@@ -1206,11 +1235,11 @@ $wgNamespaceProtection = array();
$wgNamespaceProtection[ NS_MEDIAWIKI ] = array( 'editinterface' );
/**
-* Pages in namespaces in this array can not be used as templates.
-* Elements must be numeric namespace ids.
-* Among other things, this may be useful to enforce read-restrictions
-* which may otherwise be bypassed by using the template machanism.
-*/
+ * Pages in namespaces in this array can not be used as templates.
+ * Elements must be numeric namespace ids.
+ * Among other things, this may be useful to enforce read-restrictions
+ * which may otherwise be bypassed by using the template machanism.
+ */
$wgNonincludableNamespaces = array();
/**
@@ -1252,7 +1281,6 @@ $wgAutopromote = array(
array( APCOND_EDITCOUNT, &$wgAutoConfirmCount ),
array( APCOND_AGE, &$wgAutoConfirmAge ),
),
- 'emailconfirmed' => APCOND_EMAILCONFIRMED,
);
/**
@@ -1260,22 +1288,37 @@ $wgAutopromote = array(
* groups at Special:Userrights. Example configuration:
*
* // Bureaucrat can add any group
- * $wgAddGroups['bureaucrat'] = true;
+ * $wgAddGroups['bureaucrat'] = true;
* // Bureaucrats can only remove bots and sysops
- * $wgRemoveGroups['bureaucrat'] = array( 'bot', 'sysop' );
+ * $wgRemoveGroups['bureaucrat'] = array( 'bot', 'sysop' );
* // Sysops can make bots
- * $wgAddGroups['sysop'] = array( 'bot' );
+ * $wgAddGroups['sysop'] = array( 'bot' );
* // Sysops can disable other sysops in an emergency, and disable bots
- * $wgRemoveGroups['sysop'] = array( 'sysop', 'bot' );
+ * $wgRemoveGroups['sysop'] = array( 'sysop', 'bot' );
*/
$wgAddGroups = $wgRemoveGroups = array();
+
+/**
+ * A list of available rights, in addition to the ones defined by the core.
+ * For extensions only.
+ */
+$wgAvailableRights = array();
+
/**
* Optional to restrict deletion of pages with higher revision counts
* to users with the 'bigdelete' permission. (Default given to sysops.)
*/
$wgDeleteRevisionsLimit = 0;
+/**
+ * Used to figure out if a user is "active" or not. User::isActiveEditor()
+ * sees if a user has made at least $wgActiveUserEditCount number of edits
+ * within the last $wgActiveUserDays days.
+ */
+$wgActiveUserEditCount = 30;
+$wgActiveUserDays = 30;
+
# Proxy scanner settings
#
@@ -1325,7 +1368,7 @@ $wgCacheEpoch = '20030516000000';
* to ensure that client-side caches don't keep obsolete copies of global
* styles.
*/
-$wgStyleVersion = '116';
+$wgStyleVersion = '164';
# Server-side caching:
@@ -1338,7 +1381,7 @@ $wgStyleVersion = '116';
$wgUseFileCache = false;
/** Directory where the cached page will be saved */
-$wgFileCacheDirectory = false; /// defaults to "{$wgUploadDirectory}/cache";
+$wgFileCacheDirectory = false; ///< defaults to "{$wgUploadDirectory}/cache";
/**
* When using the file cache, we can store the cached HTML gzipped to save disk
@@ -1382,17 +1425,17 @@ $wgEnotifMinorEdits = true; # UPO; false: "minor edits" on pages do not trigger
$wgEnotifImpersonal = false;
-# Maximum number of users to mail at once when using impersonal mail. Should
+# Maximum number of users to mail at once when using impersonal mail. Should
# match the limit on your mail server.
$wgEnotifMaxRecips = 500;
# Send mails via the job queue.
$wgEnotifUseJobQ = false;
-/**
+/**
* Array of usernames who will be sent a notification email for every change which occurs on a wiki
*/
-$wgUsersNotifedOnAllChanges = array();
+$wgUsersNotifiedOnAllChanges = array();
/** Show watching users in recent changes, watchlist and page history views */
$wgRCShowWatchingUsers = false; # UPO
@@ -1403,7 +1446,7 @@ $wgRCShowChangedSize = true;
/**
* If the difference between the character counts of the text
- * before and after the edit is below that value, the value will be
+ * before and after the edit is below that value, the value will be
* highlighted on the RC page.
*/
$wgRCChangedSizeThreshold = -500;
@@ -1484,6 +1527,35 @@ $wgCookiePath = '/';
$wgCookieSecure = ($wgProto == 'https');
$wgDisableCookieCheck = false;
+/**
+ * Set $wgCookiePrefix to use a custom one. Setting to false sets the default of
+ * using the database name.
+ */
+$wgCookiePrefix = false;
+
+/**
+ * Set authentication cookies to HttpOnly to prevent access by JavaScript,
+ * in browsers that support this feature. This can mitigates some classes of
+ * XSS attack.
+ *
+ * Only supported on PHP 5.2 or higher.
+ */
+$wgCookieHttpOnly = version_compare("5.2", PHP_VERSION, "<");
+
+/**
+ * If the requesting browser matches a regex in this blacklist, we won't
+ * send it cookies with HttpOnly mode, even if $wgCookieHttpOnly is on.
+ */
+$wgHttpOnlyBlacklist = array(
+ // Internet Explorer for Mac; sometimes the cookies work, sometimes
+ // they don't. It's difficult to predict, as combinations of path
+ // and expiration options affect its parsing.
+ '/^Mozilla\/4\.0 \(compatible; MSIE \d+\.\d+; Mac_PowerPC\)/',
+);
+
+/** A list of cookies that vary the cache (for use by extensions) */
+$wgCacheVaryCookies = array();
+
/** Override to customise the session name */
$wgSessionName = false;
@@ -1499,6 +1571,9 @@ $wgAllowExternalImages = false;
*/
$wgAllowExternalImagesFrom = '';
+/** Allows to move images and other media files. Experemintal, not sure if it always works */
+$wgAllowImageMoving = false;
+
/** Disable database-intensive features */
$wgMiserMode = false;
/** Disable all query pages if miser mode is on, not just some */
@@ -1520,6 +1595,7 @@ $wgJobClasses = array(
'html_cache_update' => 'HTMLCacheUpdateJob', // backwards-compatible
'sendMail' => 'EmaillingJob',
'enotifNotify' => 'EnotifNotifyJob',
+ 'fixDoubleRedirect' => 'DoubleRedirectJob',
);
/**
@@ -1577,6 +1653,46 @@ $wgDisableCounters = false;
$wgDisableTextSearch = false;
$wgDisableSearchContext = false;
+
+
+/**
+ * Set to true to have nicer highligted text in search results,
+ * by default off due to execution overhead
+ */
+$wgAdvancedSearchHighlighting = false;
+
+/**
+ * Regexp to match word boundaries, defaults for non-CJK languages
+ * should be empty for CJK since the words are not separate
+ */
+$wgSearchHighlightBoundaries = version_compare("5.1", PHP_VERSION, "<")? '[\p{Z}\p{P}\p{C}]'
+ : '[ ,.;:!?~!@#$%\^&*\(\)+=\-\\|\[\]"\'<>\n\r\/{}]'; // PHP 5.0 workaround
+
+/**
+ * Template for OpenSearch suggestions, defaults to API action=opensearch
+ *
+ * Sites with heavy load would tipically have these point to a custom
+ * PHP wrapper to avoid firing up mediawiki for every keystroke
+ *
+ * Placeholders: {searchTerms}
+ *
+ */
+$wgOpenSearchTemplate = false;
+
+/**
+ * Enable suggestions while typing in search boxes
+ * (results are passed around in OpenSearch format)
+ */
+$wgEnableMWSuggest = false;
+
+/**
+ * Template for internal MediaWiki suggestion engine, defaults to API action=opensearch
+ *
+ * Placeholders: {searchTerms}, {namespaces}, {dbname}
+ *
+ */
+$wgMWSuggestTemplate = false;
+
/**
* If you've disabled search semi-permanently, this also disables updates to the
* table. If you ever re-enable, be sure to rebuild the search table.
@@ -1632,6 +1748,11 @@ $wgAntiLockFlags = 0;
$wgDiff3 = '/usr/bin/diff3';
/**
+ * Path to the GNU diff utility.
+ */
+$wgDiff = '/usr/bin/diff';
+
+/**
* We can also compress text stored in the 'text' table. If this is set on, new
* revisions will be compressed on page save if zlib support is available. Any
* compressed revisions will be decompressed on load regardless of this setting
@@ -1679,8 +1800,8 @@ $wgCheckFileExtensions = true;
*/
$wgStrictFileExtensions = true;
-/** Warn if uploaded files are larger than this (in bytes)*/
-$wgUploadSizeWarning = 150 * 1024;
+/** Warn if uploaded files are larger than this (in bytes), or false to disable*/
+$wgUploadSizeWarning = false;
/** For compatibility with old installations set to false */
$wgPasswordSalt = true;
@@ -1717,7 +1838,7 @@ $wgSiteNotice = '';
# Images settings
#
-/**
+/**
* Plugins for media file type handling.
* Each entry in the array maps a MIME type to a class name
*/
@@ -1726,6 +1847,7 @@ $wgMediaHandlers = array(
'image/png' => 'BitmapHandler',
'image/gif' => 'BitmapHandler',
'image/x-ms-bmp' => 'BmpHandler',
+ 'image/x-bmp' => 'BmpHandler',
'image/svg+xml' => 'SvgHandler', // official
'image/svg' => 'SvgHandler', // compat
'image/vnd.djvu' => 'DjVuHandler', // official
@@ -1774,13 +1896,14 @@ $wgSVGConverters = array(
'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',
'rsvg' => '$path/rsvg -w$width -h$height $input $output',
+ 'imgserv' => '$path/imgserv-wrapper -i svg -o png -w$width $input $output',
);
/** Pick one of the above */
$wgSVGConverter = 'ImageMagick';
/** If not in the executable PATH, specify */
$wgSVGConverterPath = '';
/** Don't scale a SVG larger than this */
-$wgSVGMaxSize = 1024;
+$wgSVGMaxSize = 2048;
/**
* Don't thumbnail an image if it will use too much working memory
* Default is 50 MB if decompressed to RGBA form, which corresponds to
@@ -1809,11 +1932,11 @@ $wgThumbnailEpoch = '20030516000000';
$wgIgnoreImageErrors = false;
/**
- * Allow thumbnail rendering on page view. If this is false, a valid
- * thumbnail URL is still output, but no file will be created at
- * the target location. This may save some time if you have a
- * thumb.php or 404 handler set up which is faster than the regular
- * webserver(s).
+ * Allow thumbnail rendering on page view. If this is false, a valid
+ * thumbnail URL is still output, but no file will be created at
+ * the target location. This may save some time if you have a
+ * thumb.php or 404 handler set up which is faster than the regular
+ * webserver(s).
*/
$wgGenerateThumbnailOnParse = true;
@@ -1843,11 +1966,29 @@ $wgPutIPinRC = true;
*/
$wgRCMaxAge = 7 * 24 * 3600;
+/**
+ * Filter $wgRCLinkDays by $wgRCMaxAge to avoid showing links for numbers higher than what will be stored.
+ * Note that this is disabled by default because we sometimes do have RC data which is beyond the limit
+ * for some reason, and some users may use the high numbers to display that data which is still there.
+ */
+$wgRCFilterByAge = false;
+
+/**
+ * List of Days and Limits options to list in the Special:Recentchanges and Special:Recentchangeslinked pages.
+ */
+$wgRCLinkLimits = array( 50, 100, 250, 500 );
+$wgRCLinkDays = array( 1, 3, 7, 14, 30 );
# Send RC updates via UDP
$wgRC2UDPAddress = false;
$wgRC2UDPPort = false;
$wgRC2UDPPrefix = '';
+$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.
+$wgEnableNewpagesUserFilter = true;
#
# Copyright and credits settings
@@ -1987,7 +2128,7 @@ $wgTidyInternal = extension_loaded( 'tidy' );
$wgDebugTidy = false;
/**
- * Validate the overall output using tidy and refuse
+ * Validate the overall output using tidy and refuse
* to display the page if it's not valid.
*/
$wgValidateAllHtml = false;
@@ -2002,49 +2143,65 @@ $wgDefaultSkin = 'monobook';
* $wgDefaultUserOptions ['editsection'] = 0;
*
*/
-$wgDefaultUserOptions = array(
- 'quickbar' => 1,
- 'underline' => 2,
- 'cols' => 80,
- 'rows' => 25,
- 'searchlimit' => 20,
- 'contextlines' => 5,
- 'contextchars' => 50,
- 'skin' => false,
- 'math' => 1,
- 'rcdays' => 7,
- 'rclimit' => 50,
- 'wllimit' => 250,
- 'highlightbroken' => 1,
- 'stubthreshold' => 0,
- 'previewontop' => 1,
- 'editsection' => 1,
- 'editsectiononrightclick'=> 0,
- 'showtoc' => 1,
- 'showtoolbar' => 1,
- 'date' => 'default',
- 'imagesize' => 2,
- 'thumbsize' => 2,
- 'rememberpassword' => 0,
- 'enotifwatchlistpages' => 0,
- 'enotifusertalkpages' => 1,
- 'enotifminoredits' => 0,
- 'enotifrevealaddr' => 0,
- 'shownumberswatching' => 1,
- 'fancysig' => 0,
- 'externaleditor' => 0,
- 'externaldiff' => 0,
- 'showjumplinks' => 1,
- 'numberheadings' => 0,
- 'uselivepreview' => 0,
- 'watchlistdays' => 3.0,
+$wgDefaultUserOptions = array(
+ 'quickbar' => 1,
+ 'underline' => 2,
+ 'cols' => 80,
+ 'rows' => 25,
+ 'searchlimit' => 20,
+ 'contextlines' => 5,
+ 'contextchars' => 50,
+ 'disablesuggest' => 0,
+ 'ajaxsearch' => 0,
+ 'skin' => false,
+ 'math' => 1,
+ 'usenewrc' => 0,
+ 'rcdays' => 7,
+ 'rclimit' => 50,
+ 'wllimit' => 250,
+ 'hideminor' => 0,
+ 'highlightbroken' => 1,
+ 'stubthreshold' => 0,
+ 'previewontop' => 1,
+ 'previewonfirst' => 0,
+ 'editsection' => 1,
+ 'editsectiononrightclick' => 0,
+ 'editondblclick' => 0,
+ 'editwidth' => 0,
+ 'showtoc' => 1,
+ 'showtoolbar' => 1,
+ 'minordefault' => 0,
+ 'date' => 'default',
+ 'imagesize' => 2,
+ 'thumbsize' => 2,
+ 'rememberpassword' => 0,
+ 'enotifwatchlistpages' => 0,
+ 'enotifusertalkpages' => 1,
+ 'enotifminoredits' => 0,
+ 'enotifrevealaddr' => 0,
+ 'shownumberswatching' => 1,
+ 'fancysig' => 0,
+ 'externaleditor' => 0,
+ 'externaldiff' => 0,
+ 'showjumplinks' => 1,
+ 'numberheadings' => 0,
+ 'uselivepreview' => 0,
+ 'watchlistdays' => 3.0,
+ 'extendwatchlist' => 0,
+ 'watchlisthideminor' => 0,
+ 'watchlisthidebots' => 0,
+ 'watchlisthideown' => 0,
+ 'watchcreations' => 0,
+ 'watchdefault' => 0,
+ 'watchmoves' => 0,
+ 'watchdeletion' => 0,
);
/** Whether or not to allow and use real name fields. Defaults to true. */
$wgAllowRealName = true;
/*****************************************************************************
- * Extensions
+ * Extensions
*/
/**
@@ -2053,7 +2210,7 @@ $wgAllowRealName = true;
$wgExtensionFunctions = array();
/**
- * Extension functions for initialisation of skins. This is called somewhat earlier
+ * Extension functions for initialisation of skins. This is called somewhat earlier
* than $wgExtensionFunctions.
*/
$wgSkinExtensionFunctions = array();
@@ -2064,20 +2221,32 @@ $wgSkinExtensionFunctions = array();
* The file must create a variable called $messages.
* When the messages are needed, the extension should call wfLoadExtensionMessages().
*
- * Example:
+ * Example:
* $wgExtensionMessagesFiles['ConfirmEdit'] = dirname(__FILE__).'/ConfirmEdit.i18n.php';
*
*/
$wgExtensionMessagesFiles = array();
/**
+ * Aliases for special pages provided by extensions.
+ * Associative array mapping special page to array of aliases. First alternative
+ * for each special page will be used as the normalised name for it. English
+ * aliases will be added to the end of the list so that they always work. The
+ * file must define a variable $aliases.
+ *
+ * Example:
+ * $wgExtensionAliasesFiles['Translate'] = dirname(__FILE__).'/Translate.alias.php';
+ */
+$wgExtensionAliasesFiles = array();
+
+/**
* Parser output hooks.
* This is an associative array where the key is an extension-defined tag
* (typically the extension name), and the value is a PHP callback.
* These will be called as an OutputPageParserOutput hook, if the relevant
* tag has been registered with the parser output object.
*
- * Registration is done with $pout->addOutputHook( $tag, $data ).
+ * Registration is done with $pout->addOutputHook( $tag, $data ).
*
* The callback has the form:
* function outputHook( $outputPage, $parserOutput, $data ) { ... }
@@ -2087,7 +2256,7 @@ $wgParserOutputHooks = array();
/**
* List of valid skin names.
* The key should be the name in all lower case, the value should be a display name.
- * The default skins will be added later, by Skin::getSkinNames(). Use
+ * The default skins will be added later, by Skin::getSkinNames(). Use
* Skin::getSkinNames() as an accessor if you wish to have access to the full list.
*/
$wgValidSkinNames = array();
@@ -2096,7 +2265,7 @@ $wgValidSkinNames = array();
* Special page list.
* See the top of SpecialPage.php for documentation.
*/
-$wgSpecialPages = array();
+$wgSpecialPages = array();
/**
* Array mapping class names to filenames, for autoloading.
@@ -2111,7 +2280,8 @@ $wgAutoloadClasses = array();
* <code>
* $wgExtensionCredits[$type][] = array(
* 'name' => 'Example extension',
- * 'version' => 1.9,
+ * 'version' => 1.9,
+ * 'svn-revision' => '$LastChangedRevision: 39340 $',
* 'author' => 'Foo Barstein',
* 'url' => 'http://wwww.example.com/Example%20Extension/',
* 'description' => 'An example extension',
@@ -2161,9 +2331,12 @@ $wgExternalDiffEngine = false;
/** Use RC Patrolling to check for vandalism */
$wgUseRCPatrol = true;
-/** Use new page patrolling to check new pages on special:Newpages */
+/** Use new page patrolling to check new pages on Special:Newpages */
$wgUseNPPatrol = true;
+/** Provide syndication feeds (RSS, Atom) for, e.g., Recentchanges, Newpages */
+$wgFeed = true;
+
/** Set maximum number of results to return in syndication feeds (RSS, Atom) for
* eg Recentchanges, Newpages. */
$wgFeedLimit = 50;
@@ -2203,14 +2376,14 @@ $wgExtraNamespaces = NULL;
/**
* Namespace aliases
- * These are alternate names for the primary localised namespace names, which
- * are defined by $wgExtraNamespaces and the language file. If a page is
- * requested with such a prefix, the request will be redirected to the primary
- * name.
+ * These are alternate names for the primary localised namespace names, which
+ * are defined by $wgExtraNamespaces and the language file. If a page is
+ * requested with such a prefix, the request will be redirected to the primary
+ * name.
*
* Set this to a map from namespace names to IDs.
* Example:
- * $wgNamespaceAliases = array(
+ * $wgNamespaceAliases = array(
* 'Wikipedian' => NS_USER,
* 'Help' => 100,
* );
@@ -2274,16 +2447,16 @@ $wgBrowserBlackList = array(
* Netscape 2-4 detection
* The minor version may contain strings such as "Gold" or "SGoldC-SGI"
* Lots of non-netscape user agents have "compatible", so it's useful to check for that
- * with a negative assertion. The [UIN] identifier specifies the level of security
- * in a Netscape/Mozilla browser, checking for it rules out a number of fakers.
+ * with a negative assertion. The [UIN] identifier specifies the level of security
+ * in a Netscape/Mozilla browser, checking for it rules out a number of fakers.
* The language string is unreliable, it is missing on NS4 Mac.
- *
+ *
* Reference: http://www.psychedelix.com/agents/index.shtml
*/
'/^Mozilla\/2\.[^ ]+ [^(]*?\((?!compatible).*; [UIN]/',
'/^Mozilla\/3\.[^ ]+ [^(]*?\((?!compatible).*; [UIN]/',
'/^Mozilla\/4\.[^ ]+ [^(]*?\((?!compatible).*; [UIN]/',
-
+
/**
* MSIE on Mac OS 9 is teh sux0r, converts þ to <thorn>, ð to <eth>, Þ to <THORN> and Ð to <ETH>
*
@@ -2297,7 +2470,7 @@ $wgBrowserBlackList = array(
* @link http://en.wikipedia.org/wiki/Template%3AOS9
*/
'/^Mozilla\/4\.0 \(compatible; MSIE \d+\.\d+; Mac_PowerPC\)/',
-
+
/**
* Google wireless transcoder, seems to eat a lot of chars alive
* http://it.wikipedia.org/w/index.php?title=Luciano_Ligabue&diff=prev&oldid=8857361
@@ -2391,6 +2564,17 @@ $wgLogTypes = array( '',
'import',
'patrol',
'merge',
+ 'suppress',
+);
+
+/**
+ * This restricts log access to those who have a certain right
+ * Users without this will not see it in the option menu and can not view it
+ * Restricted logs are not added to recent changes
+ * Logs should remain non-transcludable
+ */
+$wgLogRestrictions = array(
+ 'suppress' => 'suppressionlog'
);
/**
@@ -2410,6 +2594,7 @@ $wgLogNames = array(
'import' => 'importlogpage',
'patrol' => 'patrol-log-page',
'merge' => 'mergelog',
+ 'suppress' => 'suppressionlog',
);
/**
@@ -2429,6 +2614,7 @@ $wgLogHeaders = array(
'import' => 'importlogpagetext',
'patrol' => 'patrol-log-header',
'merge' => 'mergelogpagetext',
+ 'suppress' => 'suppressionlogtext',
);
/**
@@ -2447,6 +2633,7 @@ $wgLogActions = array(
'delete/delete' => 'deletedarticle',
'delete/restore' => 'undeletedarticle',
'delete/revision' => 'revdelete-logentry',
+ 'delete/event' => 'logdelete-logentry',
'upload/upload' => 'uploadedimage',
'upload/overwrite' => 'overwroteimage',
'upload/revert' => 'uploadedimage',
@@ -2455,6 +2642,113 @@ $wgLogActions = array(
'import/upload' => 'import-logentry-upload',
'import/interwiki' => 'import-logentry-interwiki',
'merge/merge' => 'pagemerge-logentry',
+ 'suppress/revision' => 'revdelete-logentry',
+ 'suppress/file' => 'revdelete-logentry',
+ 'suppress/event' => 'logdelete-logentry',
+ 'suppress/delete' => 'suppressedarticle',
+ 'suppress/block' => 'blocklogentry',
+);
+
+/**
+ * The same as above, but here values are names of functions,
+ * not messages
+ */
+$wgLogActionsHandlers = array();
+
+/**
+ * List of special pages, followed by what subtitle they should go under
+ * at Special:SpecialPages
+ */
+$wgSpecialPageGroups = array(
+ 'DoubleRedirects' => 'maintenance',
+ 'BrokenRedirects' => 'maintenance',
+ 'Lonelypages' => 'maintenance',
+ 'Uncategorizedpages' => 'maintenance',
+ 'Uncategorizedcategories' => 'maintenance',
+ 'Uncategorizedimages' => 'maintenance',
+ 'Uncategorizedtemplates' => 'maintenance',
+ 'Unusedcategories' => 'maintenance',
+ 'Unusedimages' => 'maintenance',
+ 'Protectedpages' => 'maintenance',
+ 'Protectedtitles' => 'maintenance',
+ 'Unusedtemplates' => 'maintenance',
+ 'Withoutinterwiki' => 'maintenance',
+ 'Longpages' => 'maintenance',
+ 'Shortpages' => 'maintenance',
+ 'Ancientpages' => 'maintenance',
+ 'Deadendpages' => 'maintenance',
+ 'Wantedpages' => 'maintenance',
+ 'Wantedcategories' => 'maintenance',
+ 'Unwatchedpages' => 'maintenance',
+ 'Fewestrevisions' => 'maintenance',
+
+ 'Userlogin' => 'login',
+ 'Userlogout' => 'login',
+ 'CreateAccount' => 'login',
+
+ 'Recentchanges' => 'changes',
+ 'Recentchangeslinked' => 'changes',
+ 'Watchlist' => 'changes',
+ 'Newimages' => 'changes',
+ 'Newpages' => 'changes',
+ 'Log' => 'changes',
+
+ 'Upload' => 'media',
+ 'Imagelist' => 'media',
+ 'MIMEsearch' => 'media',
+ 'FileDuplicateSearch' => 'media',
+ 'Filepath' => 'media',
+
+ 'Listusers' => 'users',
+ 'Listgrouprights' => 'users',
+ 'Ipblocklist' => 'users',
+ 'Contributions' => 'users',
+ 'Emailuser' => 'users',
+ 'Listadmins' => 'users',
+ 'Listbots' => 'users',
+ 'Userrights' => 'users',
+ 'Blockip' => 'users',
+ 'Preferences' => 'users',
+ 'Resetpass' => 'users',
+
+ 'Mostlinked' => 'highuse',
+ 'Mostlinkedcategories' => 'highuse',
+ 'Mostlinkedtemplates' => 'highuse',
+ 'Mostcategories' => 'highuse',
+ 'Mostimages' => 'highuse',
+ 'Mostrevisions' => 'highuse',
+
+ 'Allpages' => 'pages',
+ 'Prefixindex' => 'pages',
+ 'Listredirects' => 'pages',
+ 'Categories' => 'pages',
+ 'Disambiguations' => 'pages',
+
+ 'Randompage' => 'redirects',
+ 'Randomredirect' => 'redirects',
+ 'Mypage' => 'redirects',
+ 'Mytalk' => 'redirects',
+ 'Mycontributions' => 'redirects',
+ 'Search' => 'redirects',
+
+ 'Movepage' => 'pagetools',
+ 'MergeHistory' => 'pagetools',
+ 'Revisiondelete' => 'pagetools',
+ 'Undelete' => 'pagetools',
+ 'Export' => 'pagetools',
+ 'Import' => 'pagetools',
+ 'Whatlinkshere' => 'pagetools',
+
+ 'Statistics' => 'wiki',
+ 'Version' => 'wiki',
+ 'Lockdb' => 'wiki',
+ 'Unlockdb' => 'wiki',
+ 'Allmessages' => 'wiki',
+ 'Popularpages' => 'wiki',
+
+ 'Specialpages' => 'other',
+ 'Blockme' => 'other',
+ 'Booksources' => 'other',
);
/**
@@ -2517,7 +2811,7 @@ $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
+ * Must be in the form of an array where the key part is a properly
* canonicalised text form title and the value is a robot policy.
* Example:
* $wgArticleRobotPolicies = array( 'Main Page' => 'noindex' );
@@ -2611,8 +2905,14 @@ $wgRateLimitLog = null;
/**
* Array of groups which should never trigger the rate limiter
+ *
+ * @deprecated as of 1.13.0, the preferred method is using
+ * $wgGroupPermissions[]['noratelimit']. However, this will still
+ * work if desired.
+ *
+ * $wgRateLimitsExcludedGroups = array( 'sysop', 'bureaucrat' );
*/
-$wgRateLimitsExcludedGroups = array( 'sysop', 'bureaucrat' );
+$wgRateLimitsExcludedGroups = array();
/**
* On Special:Unusedimages, consider images "used", if they are put
@@ -2634,6 +2934,7 @@ $wgExternalStores = false;
/**
* An array of external mysql servers, e.g.
* $wgExternalServers = array( 'cluster1' => array( 'srv28', 'srv29', 'srv30' ) );
+ * Used by LBFactory_Simple, may be ignored if $wgLBFactoryConf is set to another class.
*/
$wgExternalServers = array();
@@ -2641,7 +2942,7 @@ $wgExternalServers = array();
* The place to put new revisions, false to put them in the local text table.
* Part of a URL, e.g. DB://cluster1
*
- * Can be an array instead of a single string, to enable data distribution. Keys
+ * Can be an array instead of a single string, to enable data distribution. Keys
* must be consecutive integers, starting at zero. Example:
*
* $wgDefaultExternalStore = array( 'DB://cluster1', 'DB://cluster2' );
@@ -2658,15 +2959,15 @@ $wgDefaultExternalStore = false;
$wgRevisionCacheExpiry = 0;
/**
-* list of trusted media-types and mime types.
-* Use the MEDIATYPE_xxx constants to represent media types.
-* This list is used by Image::isSafeFile
-*
-* Types not listed here will have a warning about unsafe content
-* displayed on the images description page. It would also be possible
-* to use this for further restrictions, like disabling direct
-* [[media:...]] links for non-trusted formats.
-*/
+ * list of trusted media-types and mime types.
+ * Use the MEDIATYPE_xxx constants to represent media types.
+ * This list is used by Image::isSafeFile
+ *
+ * Types not listed here will have a warning about unsafe content
+ * displayed on the images description page. It would also be possible
+ * to use this for further restrictions, like disabling direct
+ * [[media:...]] links for non-trusted formats.
+ */
$wgTrustedMediaFormats= array(
MEDIATYPE_BITMAP, //all bitmap formats
MEDIATYPE_AUDIO, //all audio formats
@@ -2735,14 +3036,14 @@ $wgUpdateRowsPerQuery = 10;
$wgUseAjax = true;
/**
- * Enable auto suggestion for the search bar
+ * 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.
+ * List of Ajax-callable functions.
* Extensions acting as Ajax callbacks must register here
*/
$wgAjaxExportList = array( );
@@ -2778,6 +3079,7 @@ $wgReservedUsernames = array(
'Conversion script', // Used for the old Wikipedia software upgrade
'Maintenance script', // Maintenance scripts which perform editing, image import script
'Template namespace initialisation script', // Used in 1.2->1.3 upgrade
+ 'msg:double-redirect-fixer', // Automatic double redirect fix
);
/**
@@ -2798,7 +3100,7 @@ $wgAllowTitlesInSVG = false;
$wgContentNamespaces = array( NS_MAIN );
/**
- * Maximum amount of virtual memory available to shell processes under linux, in KB.
+ * Maximum amount of virtual memory available to shell processes under linux, in KB.
*/
$wgMaxShellMemory = 102400;
@@ -2825,7 +3127,7 @@ $wgDjvuRenderer = null;
/**
* Path of the djvutoxml executable
- * This works like djvudump except much, much slower as of version 3.5.
+ * This works like djvudump except much, much slower as of version 3.5.
*
* For now I recommend you use djvudump instead. The djvuxml output is
* probably more stable, so we'll switch back to it as soon as they fix
@@ -2868,6 +3170,15 @@ $wgEnableWriteAPI = false;
* Extension modules may override the core modules.
*/
$wgAPIModules = array();
+$wgAPIMetaModules = array();
+$wgAPIPropModules = array();
+$wgAPIListModules = array();
+
+/**
+ * Maximum amount of rows to scan in a DB query in the API
+ * The default value is generally fine
+ */
+$wgAPIMaxDBRows = 5000;
/**
* Parser test suite files to be run by parserTests.php when no specific
@@ -2884,28 +3195,23 @@ $wgParserTestFiles = array(
/**
* Break out of framesets. This can be used to prevent external sites from
- * framing your site with ads.
+ * framing your site with ads.
*/
$wgBreakFrames = false;
/**
- * Set this to an array of special page names to prevent
+ * Set this to an array of special page names to prevent
* maintenance/updateSpecialPages.php from updating those pages.
*/
$wgDisableQueryPageUpdate = false;
/**
- * Set this to false to disable cascading protection
- */
-$wgEnableCascadingProtection = true;
-
-/**
* Disable output compression (enabled by default if zlib is available)
*/
$wgDisableOutputCompression = false;
/**
- * If lag is higher than $wgSlaveLagWarning, show a warning in some special
+ * If lag is higher than $wgSlaveLagWarning, show a warning in some special
* pages (like watchlist). If the lag is higher than $wgSlaveLagCritical,
* show a more obvious warning.
*/
@@ -2915,25 +3221,76 @@ $wgSlaveLagCritical = 30;
/**
* Parser configuration. Associative array with the following members:
*
- * class The class name
- *
- * 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
- * the contents of this variable will be out-of-date. The variable can only be
- * changed during LocalSettings.php, in particular, it can't be changed during
- * an extension setup function.
+ * class The class name
+ *
+ * preprocessorClass The preprocessor class. Two classes are currently available:
+ * Preprocessor_Hash, which uses plain PHP arrays for tempoarary
+ * storage, and Preprocessor_DOM, which uses the DOM module for
+ * temporary storage. Preprocessor_DOM generally uses less memory;
+ * the speed of the two is roughly the same.
+ *
+ * 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
+ * the contents of this variable will be out-of-date. The variable can only be
+ * changed during LocalSettings.php, in particular, it can't be changed during
+ * an extension setup function.
*/
-$wgParserConf = array(
+$wgParserConf = array(
'class' => 'Parser',
+ #'preprocessorClass' => 'Preprocessor_Hash',
);
/**
- * Hooks that are used for outputting exceptions
- * Format is:
- * $wgExceptionHooks[] = $funcname
+ * Hooks that are used for outputting exceptions. Format is:
+ * $wgExceptionHooks[] = $funcname
* or:
- * $wgExceptionHooks[] = array( $class, $funcname )
+ * $wgExceptionHooks[] = array( $class, $funcname )
* Hooks should return strings or false
*/
$wgExceptionHooks = array();
+
+/**
+ * Page property link table invalidation lists. Should only be set by exten-
+ * sions.
+ */
+$wgPagePropLinkInvalidations = array(
+ 'hiddencat' => 'categorylinks',
+);
+
+/**
+ * Maximum number of links to a redirect page listed on
+ * Special:Whatlinkshere/RedirectDestination
+ */
+$wgMaxRedirectLinksRetrieved = 500;
+
+/**
+ * Maximum number of calls per parse to expensive parser functions such as
+ * PAGESINCATEGORY.
+ */
+$wgExpensiveParserFunctionLimit = 100;
+
+/**
+ * Maximum number of pages to move at once when moving subpages with a page.
+ */
+$wgMaximumMovedPages = 100;
+
+/**
+ * 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.
+ */
+$wgSitemapNamespaces = false;
+
+
+/**
+ * If user doesn't specify any edit summary when making a an edit, MediaWiki
+ * will try to automatically create one. This feature can be disabled by set-
+ * ting this variable false.
+ */
+$wgUseAutomaticEditSummaries = true;
diff --git a/includes/Defines.php b/includes/Defines.php
index 2d6aee5f..98cee57d 100644
--- a/includes/Defines.php
+++ b/includes/Defines.php
@@ -1,6 +1,7 @@
<?php
/**
* A few constants that might be needed during LocalSettings.php
+ * @file
*/
/**
@@ -84,30 +85,6 @@ define( 'MW_MATH_MODERN', 4 );
define( 'MW_MATH_MATHML', 5 );
/**#@-*/
-/**
- * User rights list
- * @deprecated
- */
-$wgAvailableRights = array(
- 'block',
- 'bot',
- 'createaccount',
- 'delete',
- 'edit',
- 'editinterface',
- 'import',
- 'importupload',
- 'move',
- 'patrol',
- 'protect',
- 'read',
- 'rollback',
- 'siteadmin',
- 'unwatchedpages',
- 'upload',
- 'userrights',
-);
-
/**#@+
* Cache type
*/
@@ -188,15 +165,15 @@ define( 'RC_MOVE_OVER_REDIRECT', 4);
*/
define( 'EDIT_NEW', 1 );
define( 'EDIT_UPDATE', 2 );
-define( 'EDIT_MINOR', 4 );
+define( 'EDIT_MINOR', 4 );
define( 'EDIT_SUPPRESS_RC', 8 );
define( 'EDIT_FORCE_BOT', 16 );
define( 'EDIT_DEFER_UPDATES', 32 );
define( 'EDIT_AUTOSUMMARY', 64 );
/**#@-*/
-/**
- * Flags for Database::makeList()
+/**
+ * Flags for Database::makeList()
* These are also available as Database class constants
*/
define( 'LIST_COMMA', 0 );
@@ -208,57 +185,7 @@ define( 'LIST_OR', 4);
/**
* Unicode and normalisation related
*/
-define( 'UNICODE_HANGUL_FIRST', 0xac00 );
-define( 'UNICODE_HANGUL_LAST', 0xd7a3 );
-
-define( 'UNICODE_HANGUL_LBASE', 0x1100 );
-define( 'UNICODE_HANGUL_VBASE', 0x1161 );
-define( 'UNICODE_HANGUL_TBASE', 0x11a7 );
-
-define( 'UNICODE_HANGUL_LCOUNT', 19 );
-define( 'UNICODE_HANGUL_VCOUNT', 21 );
-define( 'UNICODE_HANGUL_TCOUNT', 28 );
-define( 'UNICODE_HANGUL_NCOUNT', UNICODE_HANGUL_VCOUNT * UNICODE_HANGUL_TCOUNT );
-
-define( 'UNICODE_HANGUL_LEND', UNICODE_HANGUL_LBASE + UNICODE_HANGUL_LCOUNT - 1 );
-define( 'UNICODE_HANGUL_VEND', UNICODE_HANGUL_VBASE + UNICODE_HANGUL_VCOUNT - 1 );
-define( 'UNICODE_HANGUL_TEND', UNICODE_HANGUL_TBASE + UNICODE_HANGUL_TCOUNT - 1 );
-
-define( 'UNICODE_SURROGATE_FIRST', 0xd800 );
-define( 'UNICODE_SURROGATE_LAST', 0xdfff );
-define( 'UNICODE_MAX', 0x10ffff );
-define( 'UNICODE_REPLACEMENT', 0xfffd );
-
-
-define( 'UTF8_HANGUL_FIRST', "\xea\xb0\x80" /*codepointToUtf8( UNICODE_HANGUL_FIRST )*/ );
-define( 'UTF8_HANGUL_LAST', "\xed\x9e\xa3" /*codepointToUtf8( UNICODE_HANGUL_LAST )*/ );
-
-define( 'UTF8_HANGUL_LBASE', "\xe1\x84\x80" /*codepointToUtf8( UNICODE_HANGUL_LBASE )*/ );
-define( 'UTF8_HANGUL_VBASE', "\xe1\x85\xa1" /*codepointToUtf8( UNICODE_HANGUL_VBASE )*/ );
-define( 'UTF8_HANGUL_TBASE', "\xe1\x86\xa7" /*codepointToUtf8( UNICODE_HANGUL_TBASE )*/ );
-
-define( 'UTF8_HANGUL_LEND', "\xe1\x84\x92" /*codepointToUtf8( UNICODE_HANGUL_LEND )*/ );
-define( 'UTF8_HANGUL_VEND', "\xe1\x85\xb5" /*codepointToUtf8( UNICODE_HANGUL_VEND )*/ );
-define( 'UTF8_HANGUL_TEND', "\xe1\x87\x82" /*codepointToUtf8( UNICODE_HANGUL_TEND )*/ );
-
-define( 'UTF8_SURROGATE_FIRST', "\xed\xa0\x80" /*codepointToUtf8( UNICODE_SURROGATE_FIRST )*/ );
-define( 'UTF8_SURROGATE_LAST', "\xed\xbf\xbf" /*codepointToUtf8( UNICODE_SURROGATE_LAST )*/ );
-define( 'UTF8_MAX', "\xf4\x8f\xbf\xbf" /*codepointToUtf8( UNICODE_MAX )*/ );
-define( 'UTF8_REPLACEMENT', "\xef\xbf\xbd" /*codepointToUtf8( UNICODE_REPLACEMENT )*/ );
-#define( 'UTF8_REPLACEMENT', '!' );
-
-define( 'UTF8_OVERLONG_A', "\xc1\xbf" );
-define( 'UTF8_OVERLONG_B', "\xe0\x9f\xbf" );
-define( 'UTF8_OVERLONG_C', "\xf0\x8f\xbf\xbf" );
-
-# These two ranges are illegal
-define( 'UTF8_FDD0', "\xef\xb7\x90" /*codepointToUtf8( 0xfdd0 )*/ );
-define( 'UTF8_FDEF', "\xef\xb7\xaf" /*codepointToUtf8( 0xfdef )*/ );
-define( 'UTF8_FFFE', "\xef\xbf\xbe" /*codepointToUtf8( 0xfffe )*/ );
-define( 'UTF8_FFFF', "\xef\xbf\xbf" /*codepointToUtf8( 0xffff )*/ );
-
-define( 'UTF8_HEAD', false );
-define( 'UTF8_TAIL', true );
+require_once dirname(__FILE__).'/normal/UtfNormalDefines.php';
# Hook support constants
define( 'MW_SUPPORTS_EDITFILTERMERGED', 1 );
diff --git a/includes/DifferenceEngine.php b/includes/DifferenceEngine.php
index 9aa17bbb..0b4028cb 100644
--- a/includes/DifferenceEngine.php
+++ b/includes/DifferenceEngine.php
@@ -1,8 +1,6 @@
<?php
/**
- * See diff.doc
- * @todo indicate where diff.doc can be found.
- * @addtogroup DifferenceEngine
+ * @defgroup DifferenceEngine DifferenceEngine
*/
/**
@@ -15,8 +13,7 @@ define( 'MW_DIFF_VERSION', '1.11a' );
/**
* @todo document
- * @public
- * @addtogroup DifferenceEngine
+ * @ingroup DifferenceEngine
*/
class DifferenceEngine {
/**#@+
@@ -40,7 +37,7 @@ class DifferenceEngine {
* @param $rcid Integer: ??? FIXME (default 0)
* @param $refreshCache boolean If set, refreshes the diff cache
*/
- function DifferenceEngine( $titleObj = null, $old = 0, $new = 0, $rcid = 0, $refreshCache = false ) {
+ function __construct( $titleObj = null, $old = 0, $new = 0, $rcid = 0, $refreshCache = false ) {
$this->mTitle = $titleObj;
wfDebug("DifferenceEngine old '$old' new '$new' rcid '$rcid'\n");
@@ -72,10 +69,13 @@ class DifferenceEngine {
$this->mRefreshCache = $refreshCache;
}
+ function getTitle() {
+ return $this->mTitle;
+ }
+
function showDiffPage( $diffOnly = false ) {
global $wgUser, $wgOut, $wgUseExternalEditor, $wgUseRCPatrol;
- $fname = 'DifferenceEngine::showDiffPage';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
# If external diffs are enabled both globally and for the user,
# we'll use the application/x-external-editor interface to call
@@ -108,13 +108,14 @@ CONTROL;
$wgOut->setArticleFlag( false );
if ( ! $this->loadRevisionData() ) {
- $t = $this->mTitle->getPrefixedText() . " (Diff: {$this->mOldid}, {$this->mNewid})";
+ $t = $this->mTitle->getPrefixedText();
+ $d = wfMsgExt( 'missingarticle-diff', array( 'escape' ), $this->mOldid, $this->mNewid );
$wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
- $wgOut->addWikiMsg( 'missingarticle', "<nowiki>$t</nowiki>" );
- wfProfileOut( $fname );
+ $wgOut->addWikiMsg( 'missing-article', "<nowiki>$t</nowiki>", $d );
+ wfProfileOut( __METHOD__ );
return;
}
-
+
wfRunHooks( 'DiffViewHeader', array( $this, $this->mOldRev, $this->mNewRev ) );
if ( $this->mNewRev->isCurrent() ) {
@@ -127,7 +128,7 @@ CONTROL;
if ( $this->mOldid === false ) {
$this->showFirstRevision();
$this->renderNewRevision(); // should we respect $diffOnly here or not?
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return;
}
@@ -146,18 +147,20 @@ CONTROL;
if ( !( $this->mOldPage->userCanRead() && $this->mNewPage->userCanRead() ) ) {
$wgOut->loginToUse();
$wgOut->output();
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
exit;
}
$sk = $wgUser->getSkin();
- if ( $this->mNewRev->isCurrent() && $wgUser->isAllowed('rollback') ) {
+ // Check if page is editable
+ $editable = $this->mNewRev->getTitle()->userCan( 'edit' );
+ if ( $editable && $this->mNewRev->isCurrent() && $wgUser->isAllowed( 'rollback' ) ) {
$rollback = '&nbsp;&nbsp;&nbsp;' . $sk->generateRollback( $this->mNewRev );
} else {
$rollback = '';
}
-
+
// Prepare a change patrol link, if applicable
if( $wgUseRCPatrol && $wgUser->isAllowed( 'patrol' ) ) {
// If we've been given an explicit change identifier, use it; saves time
@@ -186,11 +189,11 @@ CONTROL;
}
// Build the link
if( $rcid ) {
- $patrol = ' [' . $sk->makeKnownLinkObj(
+ $patrol = ' <span class="patrollink">[' . $sk->makeKnownLinkObj(
$this->mTitle,
wfMsgHtml( 'markaspatrolleddiff' ),
"action=markpatrolled&rcid={$rcid}"
- ) . ']';
+ ) . ']</span>';
} else {
$patrol = '';
}
@@ -211,21 +214,19 @@ CONTROL;
$newminor = '';
if ($this->mOldRev->mMinorEdit == 1) {
- $oldminor = wfElement( 'span', array( 'class' => 'minor' ),
- wfMsg( 'minoreditletter') ) . ' ';
+ $oldminor = Xml::span( wfMsg( 'minoreditletter'), 'minor' ) . ' ';
}
if ($this->mNewRev->mMinorEdit == 1) {
- $newminor = wfElement( 'span', array( 'class' => 'minor' ),
- wfMsg( 'minoreditletter') ) . ' ';
+ $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');
+ $ldel = wfMsgHtml('rev-delundel');
} else {
$ldel = $sk->makeKnownLinkObj( $revdel,
wfMsgHtml('rev-delundel'),
@@ -239,10 +240,10 @@ CONTROL;
// 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');
+ $rdel = wfMsgHtml('rev-delundel');
} else if( !$this->mNewRev->userCan( Revision::DELETED_RESTRICTED ) ) {
// If revision was hidden from sysops
- $rdel = wfMsgHtml('rev-delundel');
+ $rdel = wfMsgHtml('rev-delundel');
} else {
$rdel = $sk->makeKnownLinkObj( $revdel,
wfMsgHtml('rev-delundel'),
@@ -269,7 +270,7 @@ CONTROL;
if ( !$diffOnly )
$this->renderNewRevision();
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
}
/**
@@ -277,8 +278,7 @@ CONTROL;
*/
function renderNewRevision() {
global $wgOut;
- $fname = 'DifferenceEngine::renderNewRevision';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
$wgOut->addHTML( "<hr /><h2>{$this->mPagetitle}</h2>\n" );
#add deleted rev tag if needed
@@ -316,7 +316,7 @@ CONTROL;
$wgOut->parserOptions()->setEditSection( $oldEditSectionSetting );
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
}
/**
@@ -325,18 +325,16 @@ CONTROL;
*/
function showFirstRevision() {
global $wgOut, $wgUser;
-
- $fname = 'DifferenceEngine::showFirstRevision';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
# Get article text from the DB
#
if ( ! $this->loadNewText() ) {
- $t = $this->mTitle->getPrefixedText() . " (Diff: {$this->mOldid}, " .
- "{$this->mNewid})";
+ $t = $this->mTitle->getPrefixedText();
+ $d = wfMsgExt( 'missingarticle-diff', array( 'escape' ), $this->mOldid, $this->mNewid );
$wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
- $wgOut->addWikiMsg( 'missingarticle', "<nowiki>$t</nowiki>" );
- wfProfileOut( $fname );
+ $wgOut->addWikiMsg( 'missing-article', "<nowiki>$t</nowiki>", $d );
+ wfProfileOut( __METHOD__ );
return;
}
if ( $this->mNewRev->isCurrent() ) {
@@ -348,7 +346,7 @@ CONTROL;
if ( !( $this->mTitle->userCanRead() ) ) {
$wgOut->loginToUse();
$wgOut->output();
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
exit;
}
@@ -367,7 +365,7 @@ CONTROL;
$wgOut->setSubtitle( wfMsg( 'difference' ) );
$wgOut->setRobotpolicy( 'noindex,nofollow' );
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
}
/**
@@ -378,7 +376,7 @@ CONTROL;
global $wgOut;
$diff = $this->getDiff( $otitle, $ntitle );
if ( $diff === false ) {
- $wgOut->addWikiMsg( 'missingarticle', "<nowiki>(fixme, bug)</nowiki>" );
+ $wgOut->addWikiMsg( 'missing-article', "<nowiki>(fixme, bug)</nowiki>", '' );
return false;
} else {
$this->showDiffStyle();
@@ -386,14 +384,14 @@ CONTROL;
return true;
}
}
-
+
/**
* Add style sheets and supporting JS for diff display.
*/
function showDiffStyle() {
global $wgStylePath, $wgStyleVersion, $wgOut;
$wgOut->addStyle( 'common/diff.css' );
-
+
// JS is needed to detect old versions of Mozilla to work around an annoyance bug.
$wgOut->addScript( "<script type=\"text/javascript\" src=\"$wgStylePath/common/diff.js?$wgStyleVersion\"></script>" );
}
@@ -422,9 +420,13 @@ CONTROL;
*/
function getDiffBody() {
global $wgMemc;
- $fname = 'DifferenceEngine::getDiffBody';
- wfProfileIn( $fname );
-
+ wfProfileIn( __METHOD__ );
+ // Check if the diff should be hidden from this user
+ if ( $this->mOldRev && !$this->mOldRev->userCan(Revision::DELETED_TEXT) ) {
+ return '';
+ } else if ( $this->mNewRev && !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
+ return '';
+ }
// Cacheable?
$key = false;
if ( $this->mOldid && $this->mNewid ) {
@@ -436,7 +438,7 @@ CONTROL;
wfIncrStats( 'diff_cache_hit' );
$difftext = $this->localiseLineNumbers( $difftext );
$difftext .= "\n<!-- diff cache key $key -->\n";
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $difftext;
}
} // don't try to load but save the result
@@ -444,23 +446,14 @@ CONTROL;
// Loadtext is permission safe, this just clears out the diff
if ( !$this->loadText() ) {
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return false;
- } else if ( $this->mOldRev && !$this->mOldRev->userCan(Revision::DELETED_TEXT) ) {
- return '';
- } else if ( $this->mNewRev && !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
- return '';
}
$difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext );
-
+
// Save to cache for 7 days
- // Only do this for public revs, otherwise an admin can view the diff and a non-admin can nab it!
- if ( $this->mOldRev && $this->mOldRev->isDeleted(Revision::DELETED_TEXT) ) {
- wfIncrStats( 'diff_uncacheable' );
- } else if ( $this->mNewRev && $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
- wfIncrStats( 'diff_uncacheable' );
- } else if ( $key !== false && $difftext !== false ) {
+ if ( $key !== false && $difftext !== false ) {
wfIncrStats( 'diff_cache_miss' );
$wgMemc->set( $key, $difftext, 7*86400 );
} else {
@@ -470,7 +463,7 @@ CONTROL;
if ( $difftext !== false ) {
$difftext = $this->localiseLineNumbers( $difftext );
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $difftext;
}
@@ -480,11 +473,10 @@ CONTROL;
*/
function generateDiffBody( $otext, $ntext ) {
global $wgExternalDiffEngine, $wgContLang;
- $fname = 'DifferenceEngine::generateDiffBody';
$otext = str_replace( "\r\n", "\n", $otext );
$ntext = str_replace( "\r\n", "\n", $ntext );
-
+
if ( $wgExternalDiffEngine == 'wikidiff' ) {
# For historical reasons, external diff engine expects
# input text to be HTML-escaped already
@@ -493,20 +485,22 @@ CONTROL;
if( !function_exists( 'wikidiff_do_diff' ) ) {
dl('php_wikidiff.so');
}
- return $wgContLang->unsegementForDiff( wikidiff_do_diff( $otext, $ntext, 2 ) );
+ return $wgContLang->unsegementForDiff( wikidiff_do_diff( $otext, $ntext, 2 ) ) .
+ $this->debug( 'wikidiff1' );
}
-
+
if ( $wgExternalDiffEngine == 'wikidiff2' ) {
# Better external diff engine, the 2 may some day be dropped
# This one does the escaping and segmenting itself
if ( !function_exists( 'wikidiff2_do_diff' ) ) {
- wfProfileIn( "$fname-dl" );
+ wfProfileIn( __METHOD__ . "-dl" );
@dl('php_wikidiff2.so');
- wfProfileOut( "$fname-dl" );
+ wfProfileOut( __METHOD__ . "-dl" );
}
if ( function_exists( 'wikidiff2_do_diff' ) ) {
wfProfileIn( 'wikidiff2_do_diff' );
$text = wikidiff2_do_diff( $otext, $ntext, 2 );
+ $text .= $this->debug( 'wikidiff2' );
wfProfileOut( 'wikidiff2_do_diff' );
return $text;
}
@@ -519,12 +513,12 @@ CONTROL;
$tempFile1 = fopen( $tempName1, "w" );
if ( !$tempFile1 ) {
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return false;
}
$tempFile2 = fopen( $tempName2, "w" );
if ( !$tempFile2 ) {
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return false;
}
fwrite( $tempFile1, $otext );
@@ -532,22 +526,42 @@ CONTROL;
fclose( $tempFile1 );
fclose( $tempFile2 );
$cmd = wfEscapeShellArg( $wgExternalDiffEngine, $tempName1, $tempName2 );
- wfProfileIn( "$fname-shellexec" );
+ wfProfileIn( __METHOD__ . "-shellexec" );
$difftext = wfShellExec( $cmd );
- wfProfileOut( "$fname-shellexec" );
+ $difftext .= $this->debug( "external $wgExternalDiffEngine" );
+ wfProfileOut( __METHOD__ . "-shellexec" );
unlink( $tempName1 );
unlink( $tempName2 );
return $difftext;
}
-
+
# Native PHP diff
$ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) );
$nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) );
$diffs = new Diff( $ota, $nta );
$formatter = new TableDiffFormatter();
- return $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) );
+ return $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) ) .
+ $this->debug();
+ }
+
+ /**
+ * Generate a debug comment indicating diff generating time,
+ * server node, and generator backend.
+ */
+ protected function debug( $generator="internal" ) {
+ global $wgShowHostnames, $wgNodeName;
+ $data = array( $generator );
+ if( $wgShowHostnames ) {
+ $data[] = $wgNodeName;
+ }
+ $data[] = wfTimestamp( TS_DB );
+ return "<!-- diff generator: " .
+ implode( " ",
+ array_map(
+ "htmlspecialchars",
+ $data ) ) .
+ " -->\n";
}
-
/**
* Replace line numbers with the text in the user's language
@@ -562,19 +576,19 @@ CONTROL;
return wfMsgExt( 'lineno', array('parseinline'), $wgLang->formatNum( $matches[1] ) );
}
-
+
/**
* If there are revisions between the ones being compared, return a note saying so.
*/
function getMultiNotice() {
if ( !is_object($this->mOldRev) || !is_object($this->mNewRev) )
return '';
-
+
if( !$this->mOldPage->equals( $this->mNewPage ) ) {
// Comparing two different pages? Count would be meaningless.
return '';
}
-
+
$oldid = $this->mOldRev->getId();
$newid = $this->mNewRev->getId();
if ( $oldid > $newid ) {
@@ -601,7 +615,7 @@ CONTROL;
<col class='diff-content' />
<col class='diff-marker' />
<col class='diff-content' />
- <tr>
+ <tr valign='top'>
<td colspan='2' class='diff-otitle'>{$otitle}</td>
<td colspan='2' class='diff-ntitle'>{$ntitle}</td>
</tr>
@@ -647,28 +661,31 @@ CONTROL;
: Revision::newFromTitle( $this->mTitle );
if( !$this->mNewRev instanceof Revision )
return false;
-
+
// Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
$this->mNewid = $this->mNewRev->getId();
-
+
+ // Check if page is editable
+ $editable = $this->mNewRev->getTitle()->userCan( 'edit' );
+
// Set assorted variables
$timestamp = $wgLang->timeanddate( $this->mNewRev->getTimestamp(), true );
$this->mNewPage = $this->mNewRev->getTitle();
if( $this->mNewRev->isCurrent() ) {
- $newLink = $this->mNewPage->escapeLocalUrl();
+ $newLink = $this->mNewPage->escapeLocalUrl( 'oldid=' . $this->mNewid );
$this->mPagetitle = htmlspecialchars( wfMsg( 'currentrev' ) );
$newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit' );
- $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a> ($timestamp)"
- . " (<a href='$newEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)";
+ $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a> ($timestamp)";
+ $this->mNewtitle .= " (<a href='$newEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
} else {
$newLink = $this->mNewPage->escapeLocalUrl( 'oldid=' . $this->mNewid );
$newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mNewid );
$this->mPagetitle = wfMsgHTML( 'revisionasof', $timestamp );
- $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>"
- . " (<a href='$newEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)";
+ $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>";
+ $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>";
@@ -703,18 +720,19 @@ CONTROL;
$oldLink = $this->mOldPage->escapeLocalUrl( 'oldid=' . $this->mOldid );
$oldEdit = $this->mOldPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mOldid );
$this->mOldPagetitle = htmlspecialchars( wfMsg( 'revisionasof', $t ) );
-
+
$this->mOldtitle = "<a href='$oldLink'>{$this->mOldPagetitle}</a>"
- . " (<a href='$oldEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</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);
- if ( $this->mNewRev->userCan(Revision::DELETED_TEXT) )
+ if( $editable && !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) && !$this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
$this->mNewtitle .= " (<a href='$newUndo'>" . htmlspecialchars( wfMsg( 'editundo' ) ) . "</a>)";
-
- if ( !$this->mOldRev->userCan(Revision::DELETED_TEXT) ) {
- $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>';
+ }
+
+ if( !$this->mOldRev->userCan( Revision::DELETED_TEXT ) ) {
+ $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>';
}
}
@@ -780,7 +798,7 @@ define('USE_ASSERTS', function_exists('assert'));
/**
* @todo document
* @private
- * @addtogroup DifferenceEngine
+ * @ingroup DifferenceEngine
*/
class _DiffOp {
var $type;
@@ -803,7 +821,7 @@ class _DiffOp {
/**
* @todo document
* @private
- * @addtogroup DifferenceEngine
+ * @ingroup DifferenceEngine
*/
class _DiffOp_Copy extends _DiffOp {
var $type = 'copy';
@@ -823,7 +841,7 @@ class _DiffOp_Copy extends _DiffOp {
/**
* @todo document
* @private
- * @addtogroup DifferenceEngine
+ * @ingroup DifferenceEngine
*/
class _DiffOp_Delete extends _DiffOp {
var $type = 'delete';
@@ -841,7 +859,7 @@ class _DiffOp_Delete extends _DiffOp {
/**
* @todo document
* @private
- * @addtogroup DifferenceEngine
+ * @ingroup DifferenceEngine
*/
class _DiffOp_Add extends _DiffOp {
var $type = 'add';
@@ -859,7 +877,7 @@ class _DiffOp_Add extends _DiffOp {
/**
* @todo document
* @private
- * @addtogroup DifferenceEngine
+ * @ingroup DifferenceEngine
*/
class _DiffOp_Change extends _DiffOp {
var $type = 'change';
@@ -896,15 +914,13 @@ class _DiffOp_Change extends _DiffOp {
*
* @author Geoffrey T. Dairiki, Tim Starling
* @private
- * @addtogroup DifferenceEngine
+ * @ingroup DifferenceEngine
*/
-class _DiffEngine
-{
+class _DiffEngine {
const MAX_XREF_LENGTH = 10000;
function diff ($from_lines, $to_lines) {
- $fname = '_DiffEngine::diff';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
$n_from = sizeof($from_lines);
$n_to = sizeof($to_lines);
@@ -991,7 +1007,7 @@ class _DiffEngine
elseif ($add)
$edits[] = new _DiffOp_Add($add);
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $edits;
}
@@ -1024,8 +1040,7 @@ class _DiffEngine
* of the portions it is going to specify.
*/
function _diag ($xoff, $xlim, $yoff, $ylim, $nchunks) {
- $fname = '_DiffEngine::_diag';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
$flip = false;
if ($xlim - $xoff > $ylim - $yoff) {
@@ -1051,7 +1066,7 @@ class _DiffEngine
$numer = $xlim - $xoff + $nchunks - 1;
$x = $xoff;
for ($chunk = 0; $chunk < $nchunks; $chunk++) {
- wfProfileIn( "$fname-chunk" );
+ wfProfileIn( __METHOD__ . "-chunk" );
if ($chunk > 0)
for ($i = 0; $i <= $this->lcs; $i++)
$ymids[$i][$chunk-1] = $this->seq[$i];
@@ -1085,7 +1100,7 @@ class _DiffEngine
}
}
}
- wfProfileOut( "$fname-chunk" );
+ wfProfileOut( __METHOD__ . "-chunk" );
}
$seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
@@ -1097,19 +1112,18 @@ class _DiffEngine
}
$seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return array($this->lcs, $seps);
}
function _lcs_pos ($ypos) {
- $fname = '_DiffEngine::_lcs_pos';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
$end = $this->lcs;
if ($end == 0 || $ypos > $this->seq[$end]) {
$this->seq[++$this->lcs] = $ypos;
$this->in_seq[$ypos] = 1;
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $this->lcs;
}
@@ -1127,7 +1141,7 @@ class _DiffEngine
$this->in_seq[$this->seq[$end]] = false;
$this->seq[$end] = $ypos;
$this->in_seq[$ypos] = 1;
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $end;
}
@@ -1143,8 +1157,7 @@ class _DiffEngine
* All line numbers are origin-0 and discarded lines are not counted.
*/
function _compareseq ($xoff, $xlim, $yoff, $ylim) {
- $fname = '_DiffEngine::_compareseq';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
// Slide down the bottom initial diagonal.
while ($xoff < $xlim && $yoff < $ylim
@@ -1187,7 +1200,7 @@ class _DiffEngine
$pt1 = $pt2;
}
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
}
/* Adjust inserts/deletes of identical lines to join changes
@@ -1203,8 +1216,7 @@ class _DiffEngine
* This is extracted verbatim from analyze.c (GNU diffutils-2.7).
*/
function _shift_boundaries ($lines, &$changed, $other_changed) {
- $fname = '_DiffEngine::_shift_boundaries';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
$i = 0;
$j = 0;
@@ -1309,7 +1321,7 @@ class _DiffEngine
USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
}
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
}
}
@@ -1317,7 +1329,7 @@ class _DiffEngine
* Class representing a 'diff' between two sequences of strings.
* @todo document
* @private
- * @addtogroup DifferenceEngine
+ * @ingroup DifferenceEngine
*/
class Diff
{
@@ -1427,8 +1439,7 @@ class Diff
* This is here only for debugging purposes.
*/
function _check ($from_lines, $to_lines) {
- $fname = 'Diff::_check';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
if (serialize($from_lines) != serialize($this->orig()))
trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
if (serialize($to_lines) != serialize($this->closing()))
@@ -1450,14 +1461,14 @@ class Diff
$lcs = $this->lcs();
trigger_error('Diff okay: LCS = '.$lcs, E_USER_NOTICE);
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
}
}
/**
* @todo document, bad name.
* @private
- * @addtogroup DifferenceEngine
+ * @ingroup DifferenceEngine
*/
class MappedDiff extends Diff
{
@@ -1485,9 +1496,8 @@ 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) {
- $fname = 'MappedDiff::MappedDiff';
- wfProfileIn( $fname );
+ $mapped_from_lines, $mapped_to_lines) {
+ wfProfileIn( __METHOD__ );
assert(sizeof($from_lines) == sizeof($mapped_from_lines));
assert(sizeof($to_lines) == sizeof($mapped_to_lines));
@@ -1508,7 +1518,7 @@ class MappedDiff extends Diff
$yi += sizeof($closing);
}
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
}
}
@@ -1520,10 +1530,9 @@ class MappedDiff extends Diff
* to obtain fancier outputs.
* @todo document
* @private
- * @addtogroup DifferenceEngine
+ * @ingroup DifferenceEngine
*/
-class DiffFormatter
-{
+class DiffFormatter {
/**
* Number of leading context "lines" to preserve.
*
@@ -1547,8 +1556,7 @@ class DiffFormatter
* @return string The formatted output.
*/
function format($diff) {
- $fname = 'DiffFormatter::format';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
$xi = $yi = 1;
$block = false;
@@ -1602,13 +1610,12 @@ class DiffFormatter
$block);
$end = $this->_end_diff();
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $end;
}
function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) {
- $fname = 'DiffFormatter::_block';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
$this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen));
foreach ($edits as $edit) {
if ($edit->type == 'copy')
@@ -1623,7 +1630,7 @@ class DiffFormatter
trigger_error('Unknown edit type', E_USER_ERROR);
}
$this->_end_block();
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
}
function _start_diff() {
@@ -1677,14 +1684,13 @@ class DiffFormatter
/**
* A formatter that outputs unified diffs
- * @addtogroup DifferenceEngine
+ * @ingroup DifferenceEngine
*/
-class UnifiedDiffFormatter extends DiffFormatter
-{
+class UnifiedDiffFormatter extends DiffFormatter {
var $leading_context_lines = 2;
var $trailing_context_lines = 2;
-
+
function _added($lines) {
$this->_lines($lines, '+');
}
@@ -1702,21 +1708,17 @@ class UnifiedDiffFormatter extends DiffFormatter
/**
* A pseudo-formatter that just passes along the Diff::$edits array
- * @addtogroup DifferenceEngine
+ * @ingroup DifferenceEngine
*/
-class ArrayDiffFormatter extends DiffFormatter
-{
- function format($diff)
- {
+class ArrayDiffFormatter extends DiffFormatter {
+ function format($diff) {
$oldline = 1;
$newline = 1;
$retval = array();
foreach($diff->edits as $edit)
- switch($edit->type)
- {
+ switch($edit->type) {
case 'add':
- foreach($edit->closing as $l)
- {
+ foreach($edit->closing as $l) {
$retval[] = array(
'action' => 'add',
'new'=> $l,
@@ -1725,8 +1727,7 @@ class ArrayDiffFormatter extends DiffFormatter
}
break;
case 'delete':
- foreach($edit->orig as $l)
- {
+ foreach($edit->orig as $l) {
$retval[] = array(
'action' => 'delete',
'old' => $l,
@@ -1735,8 +1736,7 @@ class ArrayDiffFormatter extends DiffFormatter
}
break;
case 'change':
- foreach($edit->orig as $i => $l)
- {
+ foreach($edit->orig as $i => $l) {
$retval[] = array(
'action' => 'change',
'old' => $l,
@@ -1751,7 +1751,7 @@ class ArrayDiffFormatter extends DiffFormatter
$newline += count($edit->orig);
}
return $retval;
- }
+ }
}
/**
@@ -1759,12 +1759,12 @@ class ArrayDiffFormatter extends DiffFormatter
*
*/
-define('NBSP', '&#160;'); // iso-8859-x non-breaking space.
+define('NBSP', '&#160;'); // iso-8859-x non-breaking space.
/**
* @todo document
* @private
- * @addtogroup DifferenceEngine
+ * @ingroup DifferenceEngine
*/
class _HWLDF_WordAccumulator {
function _HWLDF_WordAccumulator () {
@@ -1777,10 +1777,10 @@ class _HWLDF_WordAccumulator {
function _flushGroup ($new_tag) {
if ($this->_group !== '') {
if ($this->_tag == 'ins')
- $this->_line .= '<ins class="diffchange">' .
+ $this->_line .= '<ins class="diffchange diffchange-inline">' .
htmlspecialchars ( $this->_group ) . '</ins>';
elseif ($this->_tag == 'del')
- $this->_line .= '<del class="diffchange">' .
+ $this->_line .= '<del class="diffchange diffchange-inline">' .
htmlspecialchars ( $this->_group ) . '</del>';
else
$this->_line .= htmlspecialchars ( $this->_group );
@@ -1825,27 +1825,24 @@ class _HWLDF_WordAccumulator {
/**
* @todo document
* @private
- * @addtogroup DifferenceEngine
+ * @ingroup DifferenceEngine
*/
-class WordLevelDiff extends MappedDiff
-{
+class WordLevelDiff extends MappedDiff {
const MAX_LINE_LENGTH = 10000;
function WordLevelDiff ($orig_lines, $closing_lines) {
- $fname = 'WordLevelDiff::WordLevelDiff';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
list ($orig_words, $orig_stripped) = $this->_split($orig_lines);
list ($closing_words, $closing_stripped) = $this->_split($closing_lines);
$this->MappedDiff($orig_words, $closing_words,
- $orig_stripped, $closing_stripped);
- wfProfileOut( $fname );
+ $orig_stripped, $closing_stripped);
+ wfProfileOut( __METHOD__ );
}
function _split($lines) {
- $fname = 'WordLevelDiff::_split';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
$words = array();
$stripped = array();
@@ -1872,13 +1869,12 @@ class WordLevelDiff extends MappedDiff
}
}
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return array($words, $stripped);
}
function orig () {
- $fname = 'WordLevelDiff::orig';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
$orig = new _HWLDF_WordAccumulator;
foreach ($this->edits as $edit) {
@@ -1888,13 +1884,12 @@ class WordLevelDiff extends MappedDiff
$orig->addWords($edit->orig, 'del');
}
$lines = $orig->getLines();
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $lines;
}
function closing () {
- $fname = 'WordLevelDiff::closing';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
$closing = new _HWLDF_WordAccumulator;
foreach ($this->edits as $edit) {
@@ -1904,24 +1899,30 @@ class WordLevelDiff extends MappedDiff
$closing->addWords($edit->closing, 'ins');
}
$lines = $closing->getLines();
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $lines;
}
}
/**
- * Wikipedia Table style diff formatter.
+ * Wikipedia Table style diff formatter.
* @todo document
* @private
- * @addtogroup DifferenceEngine
+ * @ingroup DifferenceEngine
*/
-class TableDiffFormatter extends DiffFormatter
-{
+class TableDiffFormatter extends DiffFormatter {
function TableDiffFormatter() {
$this->leading_context_lines = 2;
$this->trailing_context_lines = 2;
}
+ public static function escapeWhiteSpace( $msg ) {
+ $msg = preg_replace( '/^ /m', '&nbsp; ', $msg );
+ $msg = preg_replace( '/ $/m', ' &nbsp;', $msg );
+ $msg = preg_replace( '/ /', '&nbsp; ', $msg );
+ return $msg;
+ }
+
function _block_header( $xbeg, $xlen, $ybeg, $ylen ) {
$r = '<tr><td colspan="2" class="diff-lineno"><!--LINE '.$xbeg."--></td>\n" .
'<td colspan="2" class="diff-lineno"><!--LINE '.$ybeg."--></td></tr>\n";
@@ -1952,11 +1953,11 @@ class TableDiffFormatter extends DiffFormatter
function contextLine( $line ) {
return $this->wrapLine( ' ', 'diff-context', $line );
}
-
+
private function wrapLine( $marker, $class, $line ) {
if( $line !== '' ) {
// The <div> wrapper is needed for 'overflow: auto' style to scroll properly
- $line = "<div>$line</div>";
+ $line = Xml::tags( 'div', null, $this->escapeWhiteSpace( $line ) );
}
return "<td class='diff-marker'>$marker</td><td class='$class'>$line</td>";
}
@@ -1990,8 +1991,7 @@ class TableDiffFormatter extends DiffFormatter
}
function _changed( $orig, $closing ) {
- $fname = 'TableDiffFormatter::_changed';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
$diff = new WordLevelDiff( $orig, $closing );
$del = $diff->orig();
@@ -2009,9 +2009,6 @@ class TableDiffFormatter extends DiffFormatter
echo '<tr>' . $this->emptyLine() .
$this->addedLine( $line ) . "</tr>\n";
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
}
}
-
-
-
diff --git a/includes/DjVuImage.php b/includes/DjVuImage.php
index b48aaffd..8e7caf63 100644
--- a/includes/DjVuImage.php
+++ b/includes/DjVuImage.php
@@ -29,13 +29,13 @@
* File format docs are available in source package for DjVuLibre:
* http://djvulibre.djvuzone.org/
*
- * @addtogroup Media
+ * @ingroup Media
*/
class DjVuImage {
function __construct( $filename ) {
$this->mFilename = $filename;
}
-
+
/**
* Check if the given file is indeed a valid DjVu image file
* @return bool
@@ -44,27 +44,27 @@ class DjVuImage {
$info = $this->getInfo();
return $info !== false;
}
-
-
+
+
/**
* Return data in the style of getimagesize()
* @return array or false on failure
*/
public function getImageSize() {
$data = $this->getInfo();
-
+
if( $data !== false ) {
$width = $data['width'];
$height = $data['height'];
-
+
return array( $width, $height, 'DjVu',
"width=\"$width\" height=\"$height\"" );
}
return false;
}
-
+
// ---------
-
+
/**
* For debugging; dump the IFF chunk structure
*/
@@ -77,7 +77,7 @@ class DjVuImage {
$this->dumpForm( $file, $chunkLength, 1 );
fclose( $file );
}
-
+
private function dumpForm( $file, $length, $indent ) {
$start = ftell( $file );
$secondary = fread( $file, 4 );
@@ -90,7 +90,7 @@ class DjVuImage {
// FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables.
extract( unpack( 'a4chunk/NchunkLength', $chunkHeader ) );
echo str_repeat( ' ', $indent * 4 ) . "$chunk $chunkLength\n";
-
+
if( $chunk == 'FORM' ) {
$this->dumpForm( $file, $chunkLength, $indent + 1 );
} else {
@@ -102,7 +102,7 @@ class DjVuImage {
}
}
}
-
+
function getInfo() {
wfSuppressWarnings();
$file = fopen( $this->mFilename, 'rb' );
@@ -111,16 +111,16 @@ class DjVuImage {
wfDebug( __METHOD__ . ": missing or failed file read\n" );
return false;
}
-
+
$header = fread( $file, 16 );
$info = false;
-
+
if( strlen( $header ) < 16 ) {
wfDebug( __METHOD__ . ": too short file header\n" );
} else {
// FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables.
extract( unpack( 'a4magic/a4form/NformLength/a4subtype', $header ) );
-
+
if( $magic != 'AT&T' ) {
wfDebug( __METHOD__ . ": not a DjVu file\n" );
} elseif( $subtype == 'DJVU' ) {
@@ -136,7 +136,7 @@ class DjVuImage {
fclose( $file );
return $info;
}
-
+
private function readChunk( $file ) {
$header = fread( $file, 8 );
if( strlen( $header ) < 8 ) {
@@ -147,16 +147,16 @@ class DjVuImage {
return array( $chunk, $length );
}
}
-
+
private function skipChunk( $file, $chunkLength ) {
fseek( $file, $chunkLength, SEEK_CUR );
-
+
if( $chunkLength & 0x01 == 1 && !feof( $file ) ) {
// padding byte
fseek( $file, 1, SEEK_CUR );
}
}
-
+
private function getMultiPageInfo( $file, $formLength ) {
// For now, we'll just look for the first page in the file
// and report its information, hoping others are the same size.
@@ -166,7 +166,7 @@ class DjVuImage {
if( !$chunk ) {
break;
}
-
+
if( $chunk == 'FORM' ) {
$subtype = fread( $file, 4 );
if( $subtype == 'DJVU' ) {
@@ -179,18 +179,18 @@ class DjVuImage {
$this->skipChunk( $file, $length );
}
} while( $length != 0 && !feof( $file ) && ftell( $file ) - $start < $formLength );
-
+
wfDebug( __METHOD__ . ": multi-page DJVU file contained no pages\n" );
return false;
}
-
+
private function getPageInfo( $file, $formLength ) {
list( $chunk, $length ) = $this->readChunk( $file );
if( $chunk != 'INFO' ) {
wfDebug( __METHOD__ . ": expected INFO chunk, got '$chunk'\n" );
return false;
}
-
+
if( $length < 9 ) {
wfDebug( __METHOD__ . ": INFO should be 9 or 10 bytes, found $length\n" );
return false;
@@ -200,7 +200,7 @@ class DjVuImage {
wfDebug( __METHOD__ . ": INFO chunk cut off\n" );
return false;
}
-
+
// FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables.
extract( unpack(
'nwidth/' .
@@ -210,7 +210,7 @@ class DjVuImage {
'vresolution/' .
'Cgamma', $data ) );
# Newer files have rotation info in byte 10, but we don't use it yet.
-
+
return array(
'width' => $width,
'height' => $height,
@@ -320,14 +320,14 @@ EOT;
}
if ( preg_match( '/^ *INFO *\[\d*\] *DjVu *(\d+)x(\d+), *\w*, *(\d+) *dpi, *gamma=([0-9.-]+)/', $line, $m ) ) {
- $xml .= Xml::tags( 'OBJECT',
+ $xml .= Xml::tags( 'OBJECT',
array(
#'data' => '',
#'type' => 'image/x.djvu',
'height' => $m[2],
'width' => $m[1],
#'usemap' => '',
- ),
+ ),
"\n" .
Xml::element( 'PARAM', array( 'name' => 'DPI', 'value' => $m[3] ) ) . "\n" .
Xml::element( 'PARAM', array( 'name' => 'GAMMA', 'value' => $m[4] ) ) . "\n"
@@ -340,6 +340,3 @@ EOT;
return false;
}
}
-
-
-?>
diff --git a/includes/DoubleRedirectJob.php b/includes/DoubleRedirectJob.php
new file mode 100644
index 00000000..889beecf
--- /dev/null
+++ b/includes/DoubleRedirectJob.php
@@ -0,0 +1,166 @@
+<?php
+
+class DoubleRedirectJob extends Job {
+ var $reason, $redirTitle, $destTitleText;
+ static $user;
+
+ /**
+ * Insert jobs into the job queue to fix redirects to the given title
+ * @param string $type The reason for the fix, see message double-redirect-fixed-<reason>
+ * @param Title $redirTitle The title which has changed, redirects pointing to this title are fixed
+ */
+ public static function fixRedirects( $reason, $redirTitle, $destTitle = false ) {
+ # Need to use the master to get the redirect table updated in the same transaction
+ $dbw = wfGetDB( DB_MASTER );
+ $res = $dbw->select(
+ array( 'redirect', 'page' ),
+ array( 'page_namespace', 'page_title' ),
+ array(
+ 'page_id = rd_from',
+ 'rd_namespace' => $redirTitle->getNamespace(),
+ 'rd_title' => $redirTitle->getDBkey()
+ ), __METHOD__ );
+ if ( !$res->numRows() ) {
+ return;
+ }
+ $jobs = array();
+ foreach ( $res as $row ) {
+ $title = Title::makeTitle( $row->page_namespace, $row->page_title );
+ if ( !$title ) {
+ continue;
+ }
+
+ $jobs[] = new self( $title, array(
+ 'reason' => $reason,
+ 'redirTitle' => $redirTitle->getPrefixedDBkey() ) );
+ # Avoid excessive memory usage
+ if ( count( $jobs ) > 10000 ) {
+ Job::batchInsert( $jobs );
+ $jobs = array();
+ }
+ }
+ Job::batchInsert( $jobs );
+ }
+ function __construct( $title, $params = false, $id = 0 ) {
+ parent::__construct( 'fixDoubleRedirect', $title, $params, $id );
+ $this->reason = $params['reason'];
+ $this->redirTitle = Title::newFromText( $params['redirTitle'] );
+ $this->destTitleText = !empty( $params['destTitle'] ) ? $params['destTitle'] : '';
+ }
+
+ function run() {
+ if ( !$this->redirTitle ) {
+ $this->setLastError( 'Invalid title' );
+ return false;
+ }
+
+ $targetRev = Revision::newFromTitle( $this->title );
+ if ( !$targetRev ) {
+ wfDebug( __METHOD__.": target redirect already deleted, ignoring\n" );
+ return true;
+ }
+ $text = $targetRev->getText();
+ $currentDest = Title::newFromRedirect( $text );
+ if ( !$currentDest || !$currentDest->equals( $this->redirTitle ) ) {
+ wfDebug( __METHOD__.": Redirect has changed since the job was queued\n" );
+ return true;
+ }
+
+ # Check for a suppression tag (used e.g. in periodically archived discussions)
+ $mw = MagicWord::get( 'staticredirect' );
+ if ( $mw->match( $text ) ) {
+ wfDebug( __METHOD__.": skipping: suppressed with __STATICREDIRECT__\n" );
+ return true;
+ }
+
+ # Find the current final destination
+ $newTitle = self::getFinalDestination( $this->redirTitle );
+ if ( !$newTitle ) {
+ wfDebug( __METHOD__.": skipping: single redirect, circular redirect or invalid redirect destination\n" );
+ return true;
+ }
+ if ( $newTitle->equals( $this->redirTitle ) ) {
+ # The redirect is already right, no need to change it
+ # This can happen if the page was moved back (say after vandalism)
+ wfDebug( __METHOD__.": skipping, already good\n" );
+ }
+
+ # Preserve fragment (bug 14904)
+ $newTitle = Title::makeTitle( $newTitle->getNamespace(), $newTitle->getDBkey(),
+ $currentDest->getFragment() );
+
+ # Fix the text
+ # Remember that redirect pages can have categories, templates, etc.,
+ # so the regex has to be fairly general
+ $newText = preg_replace( '/ \[ \[ [^\]]* \] \] /x',
+ '[[' . $newTitle->getFullText() . ']]',
+ $text, 1 );
+
+ if ( $newText === $text ) {
+ $this->setLastError( 'Text unchanged???' );
+ return false;
+ }
+
+ # Save it
+ global $wgUser;
+ $oldUser = $wgUser;
+ $wgUser = $this->getUser();
+ $article = new Article( $this->title );
+ $reason = wfMsgForContent( 'double-redirect-fixed-' . $this->reason,
+ $this->redirTitle->getPrefixedText(), $newTitle->getPrefixedText() );
+ $article->doEdit( $newText, $reason, EDIT_UPDATE | EDIT_SUPPRESS_RC );
+ $wgUser = $oldUser;
+
+ return true;
+ }
+
+ /**
+ * Get the final destination of a redirect
+ * Returns false if the specified title is not a redirect, or if it is a circular redirect
+ */
+ public static function getFinalDestination( $title ) {
+ $dbw = wfGetDB( DB_MASTER );
+
+ $seenTitles = array(); # Circular redirect check
+ $dest = false;
+
+ while ( true ) {
+ $titleText = $title->getPrefixedDBkey();
+ if ( isset( $seenTitles[$titleText] ) ) {
+ wfDebug( __METHOD__, "Circular redirect detected, aborting\n" );
+ return false;
+ }
+ $seenTitles[$titleText] = true;
+
+ $row = $dbw->selectRow(
+ array( 'redirect', 'page' ),
+ array( 'rd_namespace', 'rd_title' ),
+ array(
+ 'rd_from=page_id',
+ 'page_namespace' => $title->getNamespace(),
+ 'page_title' => $title->getDBkey()
+ ), __METHOD__ );
+ if ( !$row ) {
+ # No redirect from here, chain terminates
+ break;
+ } else {
+ $dest = $title = Title::makeTitle( $row->rd_namespace, $row->rd_title );
+ }
+ }
+ return $dest;
+ }
+
+ /**
+ * Get a user object for doing edits, from a request-lifetime cache
+ */
+ function getUser() {
+ if ( !self::$user ) {
+ self::$user = User::newFromName( wfMsgForContent( 'double-redirect-fixer' ), false );
+ if ( !self::$user->isLoggedIn() ) {
+ self::$user->addToDatabase();
+ }
+ }
+ return self::$user;
+ }
+}
+
diff --git a/includes/EditPage.php b/includes/EditPage.php
index 8c3a37d4..a34964bc 100644
--- a/includes/EditPage.php
+++ b/includes/EditPage.php
@@ -1,6 +1,7 @@
<?php
/**
* Contains the EditPage class
+ * @file
*/
/**
@@ -61,6 +62,7 @@ class EditPage {
var $autoSumm = '';
var $hookError = '';
var $mPreviewTemplates;
+ var $mBaseRevision = false;
# Form values
var $save = false, $preview = false, $diff = false;
@@ -77,10 +79,10 @@ class EditPage {
public $editFormTextAfterWarn;
public $editFormTextAfterTools;
public $editFormTextBottom;
-
+
/* $didSave should be set to true whenever an article was succesfully altered. */
public $didSave = false;
-
+
public $suppressIntro = false;
/**
@@ -99,13 +101,13 @@ class EditPage {
$this->editFormTextAfterTools =
$this->editFormTextBottom = "";
}
-
+
/**
* Fetch initial editing page content.
* @private
*/
function getContent( $def_text = '' ) {
- global $wgOut, $wgRequest, $wgParser;
+ global $wgOut, $wgRequest, $wgParser, $wgMessageCache;
# Get variables from query string :P
$section = $wgRequest->getVal( 'section' );
@@ -118,7 +120,8 @@ class EditPage {
$text = '';
if( !$this->mTitle->exists() ) {
if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
- # If this is a system message, get the default text.
+ $wgMessageCache->loadAllMessages();
+ # If this is a system message, get the default text.
$text = wfMsgWeirdKey ( $this->mTitle->getText() ) ;
} else {
# If requested, preload some text.
@@ -152,39 +155,43 @@ class EditPage {
$oldrev = $undorev ? $undorev->getPrevious() : null;
}
- #Sanity check, make sure it's the right page.
+ # 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) && $undorev->getPage()==$oldrev->getPage() && $undorev->getPage()==$this->mArticle->getID() ) {
+ if( !is_null( $undorev ) && !is_null( $oldrev ) &&
+ $undorev->getPage() == $oldrev->getPage() &&
+ $undorev->getPage() == $this->mArticle->getID() &&
+ !$undorev->isDeleted( Revision::DELETED_TEXT ) &&
+ !$oldrev->isDeleted( Revision::DELETED_TEXT ) ) {
$undorev_text = $undorev->getText();
$oldrev_text = $oldrev->getText();
$currev_text = $text;
- #No use doing a merge if it's just a straight revert.
if ( $currev_text != $undorev_text ) {
- $result = wfMerge($undorev_text, $oldrev_text, $currev_text, $text);
+ $result = wfMerge( $undorev_text, $oldrev_text, $currev_text, $text );
} else {
+ # No use doing a merge if it's just a straight revert.
$text = $oldrev_text;
$result = true;
}
+ if( $result ) {
+ # Inform the user of our success and set an automatic edit summary
+ $this->editFormPageTop .= $wgOut->parse( wfMsgNoTrans( 'undo-success' ) );
+ $firstrev = $oldrev->getNext();
+ # If we just undid one rev, use an autosummary
+ if( $firstrev->mId == $undo ) {
+ $this->summary = wfMsgForContent('undo-summary', $undo, $undorev->getUserText());
+ }
+ $this->formtype = 'diff';
+ } else {
+ # Warn the user that something went wrong
+ $this->editFormPageTop .= $wgOut->parse( wfMsgNoTrans( 'undo-failure' ) );
+ }
} else {
// Failed basic sanity checks.
// Older revisions may have been removed since the link
// was created, or we may simply have got bogus input.
- $result = false;
- }
-
- if( $result ) {
- # Inform the user of our success and set an automatic edit summary
- $this->editFormPageTop .= $wgOut->parse( wfMsgNoTrans( 'undo-success' ) );
- $firstrev = $oldrev->getNext();
- # If we just undid one rev, use an autosummary
- if ( $firstrev->mId == $undo ) {
- $this->summary = wfMsgForContent('undo-summary', $undo, $undorev->getUserText());
- }
- $this->formtype = 'diff';
- } else {
- # Warn the user that something went wrong
- $this->editFormPageTop .= $wgOut->parse( wfMsgNoTrans( 'undo-failure' ) );
+ $this->editFormPageTop .= $wgOut->parse( wfMsgNoTrans( 'undo-norev' ) );
}
} else if( $section != '' ) {
if( $section == 'new' ) {
@@ -205,7 +212,7 @@ class EditPage {
* @param $preload String: the title of the page.
* @return string The contents of the page.
*/
- private function getPreloadedText($preload) {
+ protected function getPreloadedText($preload) {
if ( $preload === '' )
return '';
else {
@@ -319,6 +326,25 @@ class EditPage {
$this->mMetaData = $s ;
}
+ 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 ) {
+ $this->deletedSinceEdit = true;
+ }
+ }
+ }
+ return $this->deletedSinceEdit;
+ }
+
function submit() {
$this->edit();
}
@@ -355,29 +381,25 @@ class EditPage {
return;
}
+ $wgOut->addScriptFile( 'edit.js' );
+
if( wfReadOnly() ) {
- $wgOut->readOnlyPage( $this->getContent() );
+ $this->readOnlyPage( $this->getContent() );
wfProfileOut( __METHOD__ );
return;
}
$permErrors = $this->mTitle->getUserPermissionsErrors('edit', $wgUser);
+
if( !$this->mTitle->exists() ) {
- # We can't use array_diff here, because that considers ANY TWO
- # ARRAYS TO BE EQUAL. Thanks, PHP.
- $createErrors = $this->mTitle->getUserPermissionsErrors('create', $wgUser);
- foreach( $createErrors as $error ) {
- # in_array() actually *does* work as expected.
- if( !in_array( $error, $permErrors ) ) {
- $permErrors[] = $error;
- }
- }
+ $permErrors = array_merge( $permErrors,
+ wfArrayDiff2( $this->mTitle->getUserPermissionsErrors('create', $wgUser), $permErrors ) );
}
# Ignore some permissions errors.
$remove = array();
foreach( $permErrors as $error ) {
- if ($this->preview || $this->diff &&
+ if ( ( $this->preview || $this->diff ) &&
($error[0] == 'blockedtext' || $error[0] == 'autoblockedtext'))
{
// Don't worry about blocks when previewing/diffing
@@ -393,11 +415,11 @@ class EditPage {
}
}
}
- $permErrors = array_diff( $permErrors, $remove );
-
- if ( !empty($permErrors) ) {
+ $permErrors = wfArrayDiff2( $permErrors, $remove );
+
+ if ( $permErrors ) {
wfDebug( __METHOD__.": User can't edit\n" );
- $wgOut->readOnlyPage( $this->getContent(), true, $permErrors );
+ $this->readOnlyPage( $this->getContent(), true, $permErrors, 'edit' );
wfProfileOut( __METHOD__ );
return;
} else {
@@ -425,31 +447,10 @@ class EditPage {
$this->isCssJsSubpage = $this->mTitle->isCssJsSubpage();
$this->isValidCssJsSubpage = $this->mTitle->isValidCssJsSubpage();
- /* Notice that we can't use isDeleted, because it returns true if article is ever deleted
- * no matter it's current state
- */
- $this->deletedSinceEdit = false;
- if ( $this->edittime != '' ) {
- /* Note that we rely on logging table, which hasn't been always there,
- * but that doesn't matter, because this only applies to brand new
- * deletes. This is done on every preview and save request. Move it further down
- * to only perform it on saves
- */
- if ( $this->mTitle->isDeleted() ) {
- $this->lastDelete = $this->getLastDelete();
- if ( !is_null($this->lastDelete) ) {
- $deletetime = $this->lastDelete->log_timestamp;
- if ( ($deletetime - $this->starttime) > 0 ) {
- $this->deletedSinceEdit = true;
- }
- }
- }
- }
-
# Show applicable editing introductions
if( $this->formtype == 'initial' || $this->firsttime )
$this->showIntro();
-
+
if( $this->mTitle->isTalkPage() ) {
$wgOut->addWikiMsg( 'talkpagetext' );
}
@@ -476,7 +477,7 @@ class EditPage {
wfProfileOut( __METHOD__ );
return;
}
- if( !$this->mTitle->getArticleId() )
+ if( !$this->mTitle->getArticleId() )
wfRunHooks( 'EditFormPreloadText', array( &$this->textbox1, &$this->mTitle ) );
}
@@ -486,11 +487,28 @@ class EditPage {
}
/**
+ * Show a read-only error
+ * Parameters are the same as OutputPage:readOnlyPage()
+ * Redirect to the article page if redlink=1
+ */
+ function readOnlyPage( $source = null, $protected = false, $reasons = array(), $action = null ) {
+ global $wgRequest, $wgOut;
+ if ( $wgRequest->getBool( 'redlink' ) ) {
+ // The edit page was reached via a red link.
+ // Redirect to the article page and let them click the edit tab if
+ // they really want a permission error.
+ $wgOut->redirect( $this->mTitle->getFullUrl() );
+ } else {
+ $wgOut->readOnlyPage( $source, $protected, $reasons, $action );
+ }
+ }
+
+ /**
* Should we show a preview when the edit form is first shown?
*
* @return bool
*/
- private function previewOnOpen() {
+ protected function previewOnOpen() {
global $wgRequest, $wgUser;
if( $wgRequest->getVal( 'preview' ) == 'yes' ) {
// Explicit override from request
@@ -521,15 +539,21 @@ class EditPage {
$fname = 'EditPage::importFormData';
wfProfileIn( $fname );
+ # Section edit can come from either the form or a link
+ $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) );
+
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.
$this->textbox1 = $this->safeUnicodeInput( $request, 'wpTextbox1' );
$this->textbox2 = $this->safeUnicodeInput( $request, 'wpTextbox2' );
- $this->mMetaData = rtrim( $request->getText( 'metadata' ) );
+ $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);
$this->edittime = $request->getVal( 'wpEdittime' );
$this->starttime = $request->getVal( 'wpStarttime' );
@@ -566,7 +590,7 @@ class EditPage {
$this->preview = true;
}
}
- $this->save = ! ( $this->preview OR $this->diff );
+ $this->save = !$this->preview && !$this->diff;
if( !preg_match( '/^\d{14}$/', $this->edittime )) {
$this->edittime = null;
}
@@ -587,7 +611,7 @@ class EditPage {
$this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' );
}
- $this->autoSumm = $request->getText( 'wpAutoSummary' );
+ $this->autoSumm = $request->getText( 'wpAutoSummary' );
} else {
# Not a posted form? Start with nothing.
wfDebug( "$fname: Not a posted form.\n" );
@@ -604,13 +628,14 @@ class EditPage {
$this->minoredit = false;
$this->watchthis = false;
$this->recreate = false;
+
+ if ( $this->section == 'new' && $request->getVal( 'preloadtitle' ) ) {
+ $this->summary = $request->getVal( 'preloadtitle' );
+ }
}
$this->oldid = $request->getInt( 'oldid' );
- # Section edit can come from either the form or a link
- $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) );
-
$this->live = $request->getCheck( 'live' );
$this->editintro = $request->getText( 'editintro' );
@@ -635,7 +660,7 @@ class EditPage {
/**
* Show all applicable editing introductions
*/
- private function showIntro() {
+ protected function showIntro() {
global $wgOut, $wgUser;
if( $this->suppressIntro )
return;
@@ -648,7 +673,7 @@ class EditPage {
$ip = User::isIP( $username );
if ( $id == 0 && !$ip ) {
- $wgOut->wrapWikiMsg( '<div class="mw-userpage-userdoesnotexist error">$1</div>',
+ $wgOut->wrapWikiMsg( '<div class="mw-userpage-userdoesnotexist error">$1</div>',
array( 'userpage-userdoesnotexist', $username ) );
}
}
@@ -668,13 +693,13 @@ class EditPage {
*
* @return bool
*/
- private function showCustomIntro() {
+ protected function showCustomIntro() {
if( $this->editintro ) {
$title = Title::newFromText( $this->editintro );
if( $title instanceof Title && $title->exists() && $title->userCanRead() ) {
global $wgOut;
$revision = Revision::newFromTitle( $title );
- $wgOut->addSecondaryWikiText( $revision->getText() );
+ $wgOut->addWikiTextTitleTidy( $revision->getText(), $this->mTitle );
return true;
} else {
return false;
@@ -721,17 +746,21 @@ class EditPage {
$matches = array();
if ( $wgSpamRegex && preg_match( $wgSpamRegex, $this->textbox1, $matches ) ) {
$result['spam'] = $matches[0];
+ $ip = wfGetIP();
+ $pdbk = $this->mTitle->getPrefixedDBkey();
+ $match = str_replace( "\n", '', $matches[0] );
+ wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" );
wfProfileOut( "$fname-checks" );
wfProfileOut( $fname );
return self::AS_SPAM_ERROR;
}
- if ( $wgFilterCallback && $wgFilterCallback( $this->mTitle, $this->textbox1, $this->section ) ) {
+ if ( $wgFilterCallback && $wgFilterCallback( $this->mTitle, $this->textbox1, $this->section, $this->hookError, $this->summary ) ) {
# Error messages or other handling should be performed by the filter function
wfProfileOut( "$fname-checks" );
wfProfileOut( $fname );
return self::AS_FILTERING;
}
- if ( !wfRunHooks( 'EditFilter', array( $this, $this->textbox1, $this->section, &$this->hookError ) ) ) {
+ if ( !wfRunHooks( 'EditFilter', array( $this, $this->textbox1, $this->section, &$this->hookError, $this->summary ) ) ) {
# Error messages etc. could be handled within the hook...
wfProfileOut( "$fname-checks" );
wfProfileOut( $fname );
@@ -783,7 +812,7 @@ class EditPage {
# If the article has been deleted while editing, don't save it without
# confirmation
- if ( $this->deletedSinceEdit && !$this->recreate ) {
+ if ( $this->wasDeletedSinceLastEdit() && !$this->recreate ) {
wfProfileOut( "$fname-checks" );
wfProfileOut( $fname );
return self::AS_ARTICLE_WAS_DELETED;
@@ -809,14 +838,14 @@ class EditPage {
}
// Run post-section-merge edit filter
- if ( !wfRunHooks( 'EditFilterMerged', array( $this, $this->textbox1, &$this->hookError ) ) ) {
+ if ( !wfRunHooks( 'EditFilterMerged', array( $this, $this->textbox1, &$this->hookError, $this->summary ) ) ) {
# Error messages etc. could be handled within the hook...
wfProfileOut( $fname );
return self::AS_HOOK_ERROR;
}
$isComment = ( $this->section == 'new' );
-
+
$this->mArticle->insertNewArticle( $this->textbox1, $this->summary,
$this->minoredit, $this->watchthis, false, $isComment, $bot);
@@ -847,7 +876,7 @@ class EditPage {
}
}
}
- $userid = $wgUser->getID();
+ $userid = $wgUser->getId();
if ( $this->isConflict) {
wfDebug( "EditPage::editForm conflict! getting section '$this->section' for time '$this->edittime' (article time '" .
@@ -892,15 +921,18 @@ class EditPage {
$oldtext = $this->mArticle->getContent();
// Run post-section-merge edit filter
- if ( !wfRunHooks( 'EditFilterMerged', array( $this, $text, &$this->hookError ) ) ) {
+ if ( !wfRunHooks( 'EditFilterMerged', array( $this, $text, &$this->hookError, $this->summary ) ) ) {
# Error messages etc. could be handled within the hook...
wfProfileOut( $fname );
return self::AS_HOOK_ERROR;
}
# 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) && !Article::getRedirectAutosummary( $text )) {
+ if( $this->section != 'new' && !$this->allowBlankSummary && $wgUser->getOption( 'forceeditsummary') &&
+ 0 != strcmp($oldtext, $text) &&
+ !is_object( Title::newFromRedirect( $text ) ) # check if it's not a redirect
+ ) {
+
if( md5( $this->summary ) == $this->autoSumm ) {
$this->missingSummary = true;
wfProfileOut( $fname );
@@ -908,7 +940,7 @@ class EditPage {
}
}
- #And a similar thing for new sections
+ # And a similar thing for new sections
if( $this->section == 'new' && !$this->allowBlankSummary && $wgUser->getOption( 'forceeditsummary' ) ) {
if (trim($this->summary) == '') {
$this->missingSummary = true;
@@ -978,7 +1010,6 @@ class EditPage {
*/
function initialiseForm() {
$this->edittime = $this->mArticle->getTimestamp();
- $this->summary = '';
$this->textbox1 = $this->getContent(false);
if ($this->textbox1 === false) return false;
@@ -997,6 +1028,13 @@ class EditPage {
function showEditForm( $formCallback=null ) {
global $wgOut, $wgUser, $wgLang, $wgContLang, $wgMaxArticleSize, $wgTitle;
+ # 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))
+ return;
+
$fname = 'EditPage::showEditForm';
wfProfileIn( $fname );
@@ -1034,8 +1072,8 @@ class EditPage {
$matches );
if( !empty( $matches[2] ) ) {
global $wgParser;
- $this->summary = "/* " .
- $wgParser->stripSectionName(trim($matches[2])) .
+ $this->summary = "/* " .
+ $wgParser->stripSectionName(trim($matches[2])) .
" */ ";
}
}
@@ -1066,13 +1104,13 @@ 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 ) ) {
$wgOut->addWikiMsg( 'rev-deleted-text-permission' );
} else if( $this->mArticle->mRevision->isDeleted( Revision::DELETED_TEXT ) ) {
$wgOut->addWikiMsg( 'rev-deleted-text-view' );
}
-
+
if( !$this->mArticle->mRevision->isCurrent() ) {
$this->mArticle->setOldSubtitle( $this->mArticle->mRevision->getId() );
$wgOut->addWikiMsg( 'editingold' );
@@ -1162,17 +1200,17 @@ class EditPage {
global $wgRightsText;
if ( $wgRightsText ) {
- $copywarnMsg = array( 'copyrightwarning',
+ $copywarnMsg = array( 'copyrightwarning',
'[[' . wfMsgForContent( 'copyrightpage' ) . ']]',
$wgRightsText );
} else {
- $copywarnMsg = array( 'copyrightwarning2',
+ $copywarnMsg = array( 'copyrightwarning2',
'[[' . wfMsgForContent( 'copyrightpage' ) . ']]' );
}
if( $wgUser->getOption('showtoolbar') and !$this->isCssJsSubpage ) {
# prepare toolbar for edit buttons
- $toolbar = $this->getEditToolbar();
+ $toolbar = EditPage::getEditToolbar();
} else {
$toolbar = '';
}
@@ -1215,14 +1253,28 @@ class EditPage {
# if this is a comment, show a subject line at the top, which is also the edit summary.
# Otherwise, show a summary field at the bottom
$summarytext = htmlspecialchars( $wgContLang->recodeForEdit( $this->summary ) ); # FIXME
+
+ # If a blank edit summary was previously provided, and the appropriate
+ # user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the
+ # user being bounced back more than once in the event that a summary
+ # is not required.
+ #####
+ # 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 );
+ $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<div class='editOptions'>\n<input tabindex='1' type='text' value=\"$summarytext\" name='wpSummary' id='wpSummary' maxlength='200' size='60' /><br />";
- $editsummary = '';
- $subjectpreview = $summarytext && $this->preview ? "<div class=\"mw-summary-preview\">".wfMsg('subject-preview').':'.$sk->commentBlock( $this->summary, $this->mTitle )."</div>\n" : '';
+ $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" : '';
$summarypreview = '';
} else {
$commentsubject = '';
- $editsummary="<span id='wpSummaryLabel'><label for='wpSummary'>{$summary}:</label></span>\n<div class='editOptions'>\n<input tabindex='2' type='text' value=\"$summarytext\" name='wpSummary' id='wpSummary' maxlength='200' size='60' /><br />";
+ $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 = '';
}
@@ -1234,6 +1286,9 @@ class EditPage {
$templates = ($this->preview || $this->section != '') ? $this->mPreviewTemplates : $this->mArticle->getUsedTemplates();
$formattedtemplates = $sk->formatTemplates( $templates, $this->preview, $this->section != '');
+ $hiddencats = $this->mArticle->getHiddenCategories();
+ $formattedhiddencats = $sk->formatHiddenCategories( $hiddencats );
+
global $wgUseMetadataEdit ;
if ( $wgUseMetadataEdit ) {
$metadata = $this->mMetaData ;
@@ -1245,7 +1300,7 @@ class EditPage {
$hidden = '';
$recreate = '';
- if ($this->deletedSinceEdit) {
+ if ($this->wasDeletedSinceLastEdit()) {
if ( 'save' != $this->formtype ) {
$wgOut->addWikiMsg('deletedwhileediting');
} else {
@@ -1292,18 +1347,24 @@ END
<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";
+ }
+
$wgOut->addHTML( <<<END
$recreate
{$commentsubject}
{$subjectpreview}
{$this->editFormTextBeforeContent}
<textarea tabindex='1' accesskey="," name="wpTextbox1" id="wpTextbox1" rows='{$rows}'
-cols='{$cols}'{$ew} $hidden>
+cols='{$cols}'{$ew} $hidden>{$encodedtext}</textarea>
END
-. htmlspecialchars( $this->safeUnicodeOutput( $this->textbox1 ) ) .
-"
-</textarea>
- " );
+);
$wgOut->wrapWikiMsg( "<div id=\"editpage-copywarn\">\n$1\n</div>", $copywarnMsg );
$wgOut->addHTML( $this->editFormTextAfterWarn );
@@ -1322,18 +1383,6 @@ END
</div><!-- editButtons -->
</div><!-- editOptions -->");
- $wgOut->addHtml( '<div class="mw-editTools">' );
- $wgOut->addWikiMsgArray( 'edittools', array(), array( 'content' ) );
- $wgOut->addHtml( '</div>' );
-
- $wgOut->addHTML( $this->editFormTextAfterTools );
-
- $wgOut->addHTML( "
-<div class='templatesUsed'>
-{$formattedtemplates}
-</div>
-" );
-
/**
* To make it harder for someone to slip a user a page
* which submits an edit form to the wiki without their
@@ -1349,21 +1398,22 @@ 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>' );
- # If a blank edit summary was previously provided, and the appropriate
- # user preference is active, pass a hidden tag here. This will stop the
- # user being bounced back more than once in the event that a summary
- # is not required.
- if( $this->missingSummary ) {
- $wgOut->addHTML( "<input type=\"hidden\" name=\"wpIgnoreBlankSummary\" value=\"1\" />\n" );
- }
+ $wgOut->addHTML( $this->editFormTextAfterTools );
- # For a bit more sophisticated detection of blank summaries, hash the
- # automatic one and pass that in a hidden field.
- $autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary );
- $wgOut->addHtml( wfHidden( 'wpAutoSummary', $autosumm ) );
+ $wgOut->addHTML( "
+<div class='templatesUsed'>
+{$formattedtemplates}
+</div>
+<div class='hiddencats'>
+{$formattedhiddencats}
+</div>
+");
- if ( $this->isConflict ) {
+ if ( $this->isConflict && wfRunHooks( 'EditPageBeforeConflictDiff', array( &$this, &$wgOut ) ) ) {
$wgOut->wrapWikiMsg( '==$1==', "yourdiff" );
$de = new DifferenceEngine( $this->mTitle );
@@ -1399,7 +1449,7 @@ END
*
* @param string $text The HTML to be output for the preview.
*/
- private function showPreview( $text ) {
+ protected function showPreview( $text ) {
global $wgOut;
$wgOut->addHTML( '<div id="wikiPreview">' );
@@ -1425,10 +1475,8 @@ END
* of the preview button
*/
function doLivePreviewScript() {
- global $wgStylePath, $wgJsMimeType, $wgStyleVersion, $wgOut, $wgTitle;
- $wgOut->addHTML( '<script type="'.$wgJsMimeType.'" src="' .
- htmlspecialchars( "$wgStylePath/common/preview.js?$wgStyleVersion" ) .
- '"></script>' . "\n" );
+ global $wgOut, $wgTitle;
+ $wgOut->addScriptFile( 'preview.js' );
$liveAction = $wgTitle->getLocalUrl( 'action=submit&wpPreview=true&live=true' );
return "return !lpDoPreview(" .
"editform.wpTextbox1.value," .
@@ -1471,7 +1519,7 @@ END
* @todo document
*/
function getPreviewText() {
- global $wgOut, $wgUser, $wgTitle, $wgParser;
+ global $wgOut, $wgUser, $wgTitle, $wgParser, $wgLang, $wgContLang;
$fname = 'EditPage::getPreviewText';
wfProfileIn( $fname );
@@ -1519,21 +1567,40 @@ END
$toparse="== {$this->summary} ==\n\n".$toparse;
}
- if ( $this->mMetaData != "" ) $toparse .= "\n" . $this->mMetaData ;
+ if ( $this->mMetaData != "" ) $toparse .= "\n" . $this->mMetaData;
+
+ // 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 );
+ }
+ }
+
+
$parserOptions->setTidy(true);
$parserOptions->enableLimitReport();
- $parserOutput = $wgParser->parse( $this->mArticle->preSaveTransform( $toparse ) ."\n\n",
+ $parserOutput = $wgParser->parse( $this->mArticle->preSaveTransform( $toparse ),
$this->mTitle, $parserOptions );
$previewHTML = $parserOutput->getText();
$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() ) );
+ $wgOut->setPageTitle( wfMsg( 'editing', $wgTitle->getPrefixedText() ) );
}
foreach ( $parserOutput->getTemplates() as $ns => $template)
@@ -1551,8 +1618,15 @@ END
$previewhead.='<h2>' . htmlspecialchars( wfMsg( 'previewconflict' ) ) . "</h2>\n";
}
+ if( $wgUser->getOption( 'previewontop' ) ) {
+ // Spacer for the edit toolbar
+ $previewfoot = '<p><br /></p>';
+ } else {
+ $previewfoot = '';
+ }
+
wfProfileOut( $fname );
- return $previewhead . $previewHTML;
+ return $previewhead . $previewHTML . $previewfoot;
}
/**
@@ -1578,7 +1652,9 @@ END
$attribs = array( 'id' => 'wpTextbox1', 'name' => 'wpTextbox1', 'cols' => $cols, 'rows' => $rows, 'readonly' => 'readonly' );
$wgOut->addHtml( '<hr />' );
$wgOut->addWikiMsg( $first ? 'blockedoriginalsource' : 'blockededitsource', $this->mTitle->getPrefixedText() );
- $wgOut->addHtml( wfOpenElement( 'textarea', $attribs ) . htmlspecialchars( $source ) . wfCloseElement( 'textarea' ) );
+ # 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' ) );
}
}
@@ -1630,7 +1706,7 @@ END
$wgOut->addHtml( '<div id="spamprotected">' );
$wgOut->addWikiMsg( 'spamprotectiontext' );
if ( $match )
- $wgOut->addWikiMsg( 'spamprotectionmatch',wfEscapeWikiText( $match ) );
+ $wgOut->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) );
$wgOut->addHtml( '</div>' );
$wgOut->returnToMain( false, $wgTitle );
@@ -1647,8 +1723,7 @@ END
$db = wfGetDB( DB_MASTER );
// This is the revision the editor started from
- $baseRevision = Revision::loadFromTimestamp(
- $db, $this->mTitle, $this->edittime );
+ $baseRevision = $this->getBaseRevision();
if( is_null( $baseRevision ) ) {
wfProfileOut( $fname );
return false;
@@ -1719,10 +1794,12 @@ END
/**
* Shows a bulletin board style toolbar for common editing functions.
* It can be disabled in the user preferences.
- * The necessary JavaScript code can be found in style/wikibits.js.
+ * The necessary JavaScript code can be found in skins/common/edit.js.
+ *
+ * @return string
*/
- function getEditToolbar() {
- global $wgStylePath, $wgContLang, $wgJsMimeType;
+ static function getEditToolbar() {
+ global $wgStylePath, $wgContLang, $wgLang, $wgJsMimeType;
/**
* toolarray an array of arrays which each include the filename of
@@ -1736,93 +1813,104 @@ END
* sure these keys are not defined on the edit page.
*/
$toolarray = array(
- array( 'image' => 'button_bold.png',
- 'id' => 'mw-editbutton-bold',
- 'open' => '\'\'\'',
- 'close' => '\'\'\'',
- 'sample'=> wfMsg('bold_sample'),
- 'tip' => wfMsg('bold_tip'),
- 'key' => 'B'
+ array(
+ 'image' => $wgLang->getImageFile('button-bold'),
+ 'id' => 'mw-editbutton-bold',
+ 'open' => '\'\'\'',
+ 'close' => '\'\'\'',
+ 'sample' => wfMsg('bold_sample'),
+ 'tip' => wfMsg('bold_tip'),
+ 'key' => 'B'
),
- array( 'image' => 'button_italic.png',
- 'id' => 'mw-editbutton-italic',
- 'open' => '\'\'',
- 'close' => '\'\'',
- 'sample'=> wfMsg('italic_sample'),
- 'tip' => wfMsg('italic_tip'),
- 'key' => 'I'
+ array(
+ 'image' => $wgLang->getImageFile('button-italic'),
+ 'id' => 'mw-editbutton-italic',
+ 'open' => '\'\'',
+ 'close' => '\'\'',
+ 'sample' => wfMsg('italic_sample'),
+ 'tip' => wfMsg('italic_tip'),
+ 'key' => 'I'
),
- array( 'image' => 'button_link.png',
- 'id' => 'mw-editbutton-link',
- 'open' => '[[',
- 'close' => ']]',
- 'sample'=> wfMsg('link_sample'),
- 'tip' => wfMsg('link_tip'),
- 'key' => 'L'
+ array(
+ 'image' => $wgLang->getImageFile('button-link'),
+ 'id' => 'mw-editbutton-link',
+ 'open' => '[[',
+ 'close' => ']]',
+ 'sample' => wfMsg('link_sample'),
+ 'tip' => wfMsg('link_tip'),
+ 'key' => 'L'
),
- array( 'image' => 'button_extlink.png',
- 'id' => 'mw-editbutton-extlink',
- 'open' => '[',
- 'close' => ']',
- 'sample'=> wfMsg('extlink_sample'),
- 'tip' => wfMsg('extlink_tip'),
- 'key' => 'X'
+ array(
+ 'image' => $wgLang->getImageFile('button-extlink'),
+ 'id' => 'mw-editbutton-extlink',
+ 'open' => '[',
+ 'close' => ']',
+ 'sample' => wfMsg('extlink_sample'),
+ 'tip' => wfMsg('extlink_tip'),
+ 'key' => 'X'
),
- array( 'image' => 'button_headline.png',
- 'id' => 'mw-editbutton-headline',
- 'open' => "\n== ",
- 'close' => " ==\n",
- 'sample'=> wfMsg('headline_sample'),
- 'tip' => wfMsg('headline_tip'),
- 'key' => 'H'
+ array(
+ 'image' => $wgLang->getImageFile('button-headline'),
+ 'id' => 'mw-editbutton-headline',
+ 'open' => "\n== ",
+ 'close' => " ==\n",
+ 'sample' => wfMsg('headline_sample'),
+ 'tip' => wfMsg('headline_tip'),
+ 'key' => 'H'
),
- array( 'image' => 'button_image.png',
- 'id' => 'mw-editbutton-image',
- 'open' => '[['.$wgContLang->getNsText(NS_IMAGE).":",
- 'close' => ']]',
- 'sample'=> wfMsg('image_sample'),
- 'tip' => wfMsg('image_tip'),
- 'key' => 'D'
+ array(
+ 'image' => $wgLang->getImageFile('button-image'),
+ 'id' => 'mw-editbutton-image',
+ 'open' => '[['.$wgContLang->getNsText(NS_IMAGE).':',
+ 'close' => ']]',
+ 'sample' => wfMsg('image_sample'),
+ 'tip' => wfMsg('image_tip'),
+ 'key' => 'D'
),
- array( 'image' => 'button_media.png',
- 'id' => 'mw-editbutton-media',
- 'open' => '[['.$wgContLang->getNsText(NS_MEDIA).':',
- 'close' => ']]',
- 'sample'=> wfMsg('media_sample'),
- 'tip' => wfMsg('media_tip'),
- 'key' => 'M'
+ array(
+ 'image' => $wgLang->getImageFile('button-media'),
+ 'id' => 'mw-editbutton-media',
+ 'open' => '[['.$wgContLang->getNsText(NS_MEDIA).':',
+ 'close' => ']]',
+ 'sample' => wfMsg('media_sample'),
+ 'tip' => wfMsg('media_tip'),
+ 'key' => 'M'
),
- array( 'image' => 'button_math.png',
- 'id' => 'mw-editbutton-math',
- 'open' => "<math>",
- 'close' => "</math>",
- 'sample'=> wfMsg('math_sample'),
- 'tip' => wfMsg('math_tip'),
- 'key' => 'C'
+ array(
+ 'image' => $wgLang->getImageFile('button-math'),
+ 'id' => 'mw-editbutton-math',
+ 'open' => "<math>",
+ 'close' => "</math>",
+ 'sample' => wfMsg('math_sample'),
+ 'tip' => wfMsg('math_tip'),
+ 'key' => 'C'
),
- array( 'image' => 'button_nowiki.png',
- 'id' => 'mw-editbutton-nowiki',
- 'open' => "<nowiki>",
- 'close' => "</nowiki>",
- 'sample'=> wfMsg('nowiki_sample'),
- 'tip' => wfMsg('nowiki_tip'),
- 'key' => 'N'
+ array(
+ 'image' => $wgLang->getImageFile('button-nowiki'),
+ 'id' => 'mw-editbutton-nowiki',
+ 'open' => "<nowiki>",
+ 'close' => "</nowiki>",
+ 'sample' => wfMsg('nowiki_sample'),
+ 'tip' => wfMsg('nowiki_tip'),
+ 'key' => 'N'
),
- array( 'image' => 'button_sig.png',
- 'id' => 'mw-editbutton-signature',
- 'open' => '--~~~~',
- 'close' => '',
- 'sample'=> '',
- 'tip' => wfMsg('sig_tip'),
- 'key' => 'Y'
+ array(
+ 'image' => $wgLang->getImageFile('button-sig'),
+ 'id' => 'mw-editbutton-signature',
+ 'open' => '--~~~~',
+ 'close' => '',
+ 'sample' => '',
+ 'tip' => wfMsg('sig_tip'),
+ 'key' => 'Y'
),
- array( 'image' => 'button_hr.png',
- 'id' => 'mw-editbutton-hr',
- 'open' => "\n----\n",
- 'close' => '',
- 'sample'=> '',
- 'tip' => wfMsg('hr_tip'),
- 'key' => 'R'
+ array(
+ 'image' => $wgLang->getImageFile('button-hr'),
+ 'id' => 'mw-editbutton-hr',
+ 'open' => "\n----\n",
+ 'close' => '',
+ 'sample' => '',
+ 'tip' => wfMsg('hr_tip'),
+ 'key' => 'R'
)
);
$toolbar = "<div id='toolbar'>\n";
@@ -1841,7 +1929,7 @@ END
$sample = $tool['sample'],
$cssId = $tool['id'],
);
-
+
$paramList = implode( ',',
array_map( array( 'Xml', 'encodeJsVar' ), $params ) );
$toolbar.="addButton($paramList);\n";
@@ -1878,7 +1966,7 @@ END
);
$checkboxes['minor'] =
Xml::check( 'wpMinoredit', $checked['minor'], $attribs ) .
- "&nbsp;<label for='wpMinoredit'".$skin->tooltipAndAccesskey('minoredit').">{$minorLabel}</label>";
+ "&nbsp;<label for='wpMinoredit'".$skin->tooltip('minoredit', 'withaccess').">{$minorLabel}</label>";
}
$watchLabel = wfMsgExt('watchthis', array('parseinline'));
@@ -1891,7 +1979,7 @@ END
);
$checkboxes['watch'] =
Xml::check( 'wpWatchthis', $checked['watch'], $attribs ) .
- "&nbsp;<label for='wpWatchthis'".$skin->tooltipAndAccesskey('watch').">{$watchLabel}</label>";
+ "&nbsp;<label for='wpWatchthis'".$skin->tooltip('watch', 'withaccess').">{$watchLabel}</label>";
}
return $checkboxes;
}
@@ -1918,7 +2006,7 @@ END
'accesskey' => wfMsg('accesskey-save'),
'title' => wfMsg( 'tooltip-save' ).' ['.wfMsg( 'accesskey-save' ).']',
);
- $buttons['save'] = wfElement('input', $temp, '');
+ $buttons['save'] = Xml::element('input', $temp, '');
++$tabindex; // use the same for preview and live preview
if ( $wgLivePreview && $wgUser->getOption( 'uselivepreview' ) ) {
@@ -1932,7 +2020,7 @@ END
'title' => wfMsg( 'tooltip-preview' ).' ['.wfMsg( 'accesskey-preview' ).']',
'style' => 'display: none;',
);
- $buttons['preview'] = wfElement('input', $temp, '');
+ $buttons['preview'] = Xml::element('input', $temp, '');
$temp = array(
'id' => 'wpLivePreview',
@@ -1944,7 +2032,7 @@ END
'title' => '',
'onclick' => $this->doLivePreviewScript(),
);
- $buttons['live'] = wfElement('input', $temp, '');
+ $buttons['live'] = Xml::element('input', $temp, '');
} else {
$temp = array(
'id' => 'wpPreview',
@@ -1955,7 +2043,7 @@ END
'accesskey' => wfMsg('accesskey-preview'),
'title' => wfMsg( 'tooltip-preview' ).' ['.wfMsg( 'accesskey-preview' ).']',
);
- $buttons['preview'] = wfElement('input', $temp, '');
+ $buttons['preview'] = Xml::element('input', $temp, '');
$buttons['live'] = '';
}
@@ -1968,8 +2056,8 @@ END
'accesskey' => wfMsg('accesskey-diff'),
'title' => wfMsg( 'tooltip-diff' ).' ['.wfMsg( 'accesskey-diff' ).']',
);
- $buttons['diff'] = wfElement('input', $temp, '');
-
+ $buttons['diff'] = Xml::element('input', $temp, '');
+
wfRunHooks( 'EditPageBeforeEditButtons', array( &$this, &$buttons ) );
return $buttons;
}
@@ -2152,28 +2240,25 @@ END
$wgOut->setPageTitle( wfMsg( 'nocreatetitle' ) );
$wgOut->addWikiMsg( 'nocreatetext' );
}
-
+
/**
* If there are rows in the deletion log for this page, show them,
* along with a nice little note for the user
*
* @param OutputPage $out
*/
- private function showDeletionLog( $out ) {
- $title = $this->mTitle;
- $reader = new LogReader(
- new FauxRequest(
- array(
- 'page' => $title->getPrefixedText(),
- 'type' => 'delete',
- )
- )
- );
- if( $reader->hasRows() ) {
+ protected function showDeletionLog( $out ) {
+ 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">' );
$out->addWikiMsg( 'recreate-deleted-warn' );
- $viewer = new LogViewer( $reader );
- $viewer->showList( $out );
+ $out->addHTML(
+ $loglist->beginLogEventsList() .
+ $pager->getBody() .
+ $loglist->endLogEventsList()
+ );
$out->addHtml( '</div>' );
}
}
@@ -2187,7 +2272,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 ) {
$this->didSave = true;
}
@@ -2237,7 +2322,7 @@ END
case self::AS_NO_CREATE_PERMISSION;
$this->noCreatePermission();
return;
-
+
case self::AS_BLANK_ARTICLE:
$wgOut->redirect( $wgTitle->getFullURL() );
return false;
@@ -2247,4 +2332,15 @@ END
return false;
}
}
+
+ function getBaseRevision() {
+ if ($this->mBaseRevision == false) {
+ $db = wfGetDB( DB_MASTER );
+ $baseRevision = Revision::loadFromTimestamp(
+ $db, $this->mTitle, $this->edittime );
+ return $this->mBaseRevision = $baseRevision;
+ } else {
+ return $this->mBaseRevision;
+ }
+ }
}
diff --git a/includes/EmaillingJob.php b/includes/EmaillingJob.php
index 73d71c56..380c8982 100644
--- a/includes/EmaillingJob.php
+++ b/includes/EmaillingJob.php
@@ -3,6 +3,8 @@
/**
* Old job used for sending single notification emails;
* kept for backwards-compatibility
+ *
+ * @ingroup JobQueue
*/
class EmaillingJob extends Job {
@@ -20,6 +22,5 @@ class EmaillingJob extends Job {
);
return true;
}
-
-}
+}
diff --git a/includes/EnotifNotifyJob.php b/includes/EnotifNotifyJob.php
index 70d1de69..31fcb0d5 100644
--- a/includes/EnotifNotifyJob.php
+++ b/includes/EnotifNotifyJob.php
@@ -2,6 +2,8 @@
/**
* Job for email notification mails
+ *
+ * @ingroup JobQueue
*/
class EnotifNotifyJob extends Job {
@@ -11,16 +13,22 @@ class EnotifNotifyJob extends Job {
function run() {
$enotif = new EmailNotification();
+ // Get the user from ID (rename safe). Anons are 0, so defer to name.
+ if( isset($this->params['editorID']) && $this->params['editorID'] ) {
+ $editor = User::newFromId( $this->params['editorID'] );
+ // B/C, only the name might be given.
+ } else {
+ $editor = User::newFromName( $this->params['editor'], false );
+ }
$enotif->actuallyNotifyOnPageChange(
- User::newFromName( $this->params['editor'], false ),
- $this->title,
- $this->params['timestamp'],
- $this->params['summary'],
- $this->params['minorEdit'],
- $this->params['oldid']
+ $editor,
+ $this->title,
+ $this->params['timestamp'],
+ $this->params['summary'],
+ $this->params['minorEdit'],
+ $this->params['oldid']
);
return true;
}
-
-}
+}
diff --git a/includes/Exception.php b/includes/Exception.php
index 2fd54352..74820204 100644
--- a/includes/Exception.php
+++ b/includes/Exception.php
@@ -1,24 +1,43 @@
<?php
+/**
+ * @defgroup Exception Exception
+ */
/**
* MediaWiki exception
- * @addtogroup Exception
+ * @ingroup Exception
*/
-class MWException extends Exception
-{
+class MWException extends Exception {
+
+ /**
+ * Should the exception use $wgOut to output the error ?
+ * @return bool
+ */
function useOutputPage() {
- return !empty( $GLOBALS['wgFullyInitialised'] ) &&
- !empty( $GLOBALS['wgArticle'] ) && !empty( $GLOBALS['wgTitle'] );
+ return !empty( $GLOBALS['wgFullyInitialised'] ) &&
+ ( !empty( $GLOBALS['wgArticle'] ) || ( !empty( $GLOBALS['wgOut'] ) && !$GLOBALS['wgOut']->isArticle() ) ) &&
+ !empty( $GLOBALS['wgTitle'] );
}
+ /**
+ * Can the extension use wfMsg() to get i18n messages ?
+ * @return bool
+ */
function useMessageCache() {
global $wgLang;
return is_object( $wgLang );
}
+ /**
+ * Run hook to allow extensions to modify the text of the exception
+ *
+ * @param String $name class name of the exception
+ * @param Array $args arguments to pass to the callback functions
+ * @return mixed string to output or null if any hook has been called
+ */
function runHooks( $name, $args = array() ) {
global $wgExceptionHooks;
- if( !isset( $wgExceptionHooks ) || !is_array( $wgExceptionHooks ) )
+ if( !isset( $wgExceptionHooks ) || !is_array( $wgExceptionHooks ) )
return; // Just silently ignore
if( !array_key_exists( $name, $wgExceptionHooks ) || !is_array( $wgExceptionHooks[ $name ] ) )
return;
@@ -36,7 +55,15 @@ class MWException extends Exception
}
}
- /** Get a message from i18n */
+ /**
+ * Get a message from i18n
+ *
+ * @param String $key message name
+ * @param String $fallback default message if the message cache can't be
+ * called by the exception
+ * The function also has other parameters that are arguments for the message
+ * @return String message with arguments replaced
+ */
function msg( $key, $fallback /*[, params...] */ ) {
$args = array_slice( func_get_args(), 2 );
if ( $this->useMessageCache() ) {
@@ -46,11 +73,17 @@ class MWException extends Exception
}
}
- /* If wgShowExceptionDetails, return a HTML message with a backtrace to the error. */
+ /**
+ * If $wgShowExceptionDetails is true, return a HTML message with a
+ * backtrace to the error, otherwise show a message to ask to set it to true
+ * to show that information.
+ *
+ * @return String html to output
+ */
function getHTML() {
global $wgShowExceptionDetails;
if( $wgShowExceptionDetails ) {
- return '<p>' . htmlspecialchars( $this->getMessage() ) .
+ return '<p>' . htmlspecialchars( $this->getMessage() ) .
'</p><p>Backtrace:</p><p>' . nl2br( htmlspecialchars( $this->getTraceAsString() ) ) .
"</p>\n";
} else {
@@ -60,15 +93,18 @@ class MWException extends Exception
}
}
- /* If wgShowExceptionDetails, return a text message with a backtrace to the error */
+ /**
+ * If $wgShowExceptionDetails is true, return a text message with a
+ * backtrace to the error.
+ */
function getText() {
global $wgShowExceptionDetails;
if( $wgShowExceptionDetails ) {
return $this->getMessage() .
"\nBacktrace:\n" . $this->getTraceAsString() . "\n";
} else {
- return "<p>Set <tt>\$wgShowExceptionDetails = true;</tt> " .
- "in LocalSettings.php to show detailed debugging information.</p>";
+ return "Set \$wgShowExceptionDetails = true; " .
+ "in LocalSettings.php to show detailed debugging information.\n";
}
}
@@ -82,8 +118,11 @@ class MWException extends Exception
}
}
- /** Return the requested URL and point to file and line number from which the
+ /**
+ * Return the requested URL and point to file and line number from which the
* exception occured
+ *
+ * @return string
*/
function getLogMessage() {
global $wgRequest;
@@ -119,22 +158,27 @@ class MWException extends Exception
}
}
- /* Output a report about the exception and takes care of formatting.
+ /**
+ * Output a report about the exception and takes care of formatting.
* It will be either HTML or plain text based on $wgCommandLineMode.
*/
function report() {
global $wgCommandLineMode;
+ $log = $this->getLogMessage();
+ if ( $log ) {
+ wfDebugLog( 'exception', $log );
+ }
if ( $wgCommandLineMode ) {
fwrite( STDERR, $this->getText() );
} else {
- $log = $this->getLogMessage();
- if ( $log ) {
- wfDebugLog( 'exception', $log );
- }
$this->reportHTML();
}
}
+ /**
+ * Send headers and output the beginning of the html page if not using
+ * $wgOut to output the exception.
+ */
function htmlHeader() {
global $wgLogo, $wgSitename, $wgOutputEncoding;
@@ -155,6 +199,9 @@ class MWException extends Exception
";
}
+ /**
+ * print the end of the html page if not using $wgOut.
+ */
function htmlFooter() {
echo "</body></html>";
}
@@ -163,7 +210,7 @@ class MWException extends Exception
/**
* Exception class which takes an HTML error message, and does not
* produce a backtrace. Replacement for OutputPage::fatalError().
- * @addtogroup Exception
+ * @ingroup Exception
*/
class FatalError extends MWException {
function getHTML() {
@@ -176,11 +223,11 @@ class FatalError extends MWException {
}
/**
- * @addtogroup Exception
+ * @ingroup Exception
*/
class ErrorPageError extends MWException {
public $title, $msg;
-
+
/**
* Note: these arguments are keys into wfMsg(), not text!
*/
@@ -256,5 +303,3 @@ function wfExceptionHandler( $e ) {
// Exit value should be nonzero for the benefit of shell jobs
exit( 1 );
}
-
-
diff --git a/includes/Exif.php b/includes/Exif.php
index d98a8e0d..bd93eb76 100644
--- a/includes/Exif.php
+++ b/includes/Exif.php
@@ -1,7 +1,6 @@
<?php
/**
- * @addtogroup Media
- *
+ * @ingroup Media
* @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
@@ -26,7 +25,7 @@
/**
* @todo document (e.g. one-sentence class-overview description)
- * @addtogroup Media
+ * @ingroup Media
*/
class Exif {
//@{
@@ -431,7 +430,7 @@ class Exif {
if ( is_array( $in ) ) {
return false;
}
-
+
if ( preg_match( "/[^\x0a\x20-\x7e]/", $in ) ) {
$this->debug( $in, __FUNCTION__, 'found a character not in our whitelist' );
return false;
@@ -557,8 +556,8 @@ class Exif {
*
* @private
*
- * @param $in Mixed:
- * @param $fname String:
+ * @param $in Mixed:
+ * @param $fname String:
* @param $action Mixed: , default NULL.
*/
function debug( $in, $fname, $action = NULL ) {
@@ -604,7 +603,7 @@ class Exif {
/**
* @todo document (e.g. one-sentence class-overview description)
- * @addtogroup Media
+ * @ingroup Media
*/
class FormatExif {
/**
@@ -1130,5 +1129,3 @@ define( 'MW_EXIF_RATIONAL', Exif::RATIONAL );
define( 'MW_EXIF_UNDEFINED', Exif::UNDEFINED );
define( 'MW_EXIF_SLONG', Exif::SLONG );
define( 'MW_EXIF_SRATIONAL', Exif::SRATIONAL );
-
-
diff --git a/includes/Export.php b/includes/Export.php
index 69d88fc6..7d0a824e 100644
--- a/includes/Export.php
+++ b/includes/Export.php
@@ -17,15 +17,19 @@
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# http://www.gnu.org/copyleft/gpl.html
+/**
+ * @defgroup Dump Dump
+ */
/**
- *
- * @addtogroup SpecialPage
+ * @ingroup SpecialPage Dump
*/
class WikiExporter {
var $list_authors = false ; # Return distinct author list (when not returning full history)
var $author_list = "" ;
+ var $dumpUploads = false;
+
const FULL = 0;
const CURRENT = 1;
@@ -42,13 +46,13 @@ class WikiExporter {
* make additional queries to pull source data while the
* main query is still running.
*
- * @param Database $db
- * @param mixed $history one of WikiExporter::FULL or WikiExporter::CURRENT, or an
- * associative array:
- * offset: non-inclusive offset at which to start the query
- * limit: maximum number of rows to return
- * dir: "asc" or "desc" timestamp order
- * @param int $buffer one of WikiExporter::BUFFER or WikiExporter::STREAM
+ * @param $db Database
+ * @param $history Mixed: one of WikiExporter::FULL or WikiExporter::CURRENT,
+ * or an associative array:
+ * offset: non-inclusive offset at which to start the query
+ * limit: maximum number of rows to return
+ * dir: "asc" or "desc" timestamp order
+ * @param $buffer Int: one of WikiExporter::BUFFER or WikiExporter::STREAM
*/
function __construct( &$db, $history = WikiExporter::CURRENT,
$buffer = WikiExporter::BUFFER, $text = WikiExporter::TEXT ) {
@@ -65,7 +69,7 @@ class WikiExporter {
* various row objects and XML output for filtering. Filters
* can be chained or used as callbacks.
*
- * @param mixed $callback
+ * @param $sink mixed
*/
function setOutputSink( &$sink ) {
$this->sink =& $sink;
@@ -93,8 +97,8 @@ class WikiExporter {
/**
* Dumps a series of page and revision records for those pages
* in the database falling within the page_id range given.
- * @param int $start Inclusive lower limit (this id is included)
- * @param int $end Exclusive upper limit (this id is not included)
+ * @param $start Int: inclusive lower limit (this id is included)
+ * @param $end Int: Exclusive upper limit (this id is not included)
* If 0, no upper limit.
*/
function pagesByRange( $start, $end ) {
@@ -106,7 +110,7 @@ class WikiExporter {
}
/**
- * @param Title $title
+ * @param $title Title
*/
function pageByTitle( $title ) {
return $this->dumpFrom(
@@ -141,18 +145,18 @@ class WikiExporter {
$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 ;
$result = $this->db->query( $sql, $fname );
$resultset = $this->db->resultObject( $result );
while( $row = $resultset->fetchObject() ) {
- $this->author_list .= "<contributor>" .
- "<username>" .
- htmlentities( $row->rev_user_text ) .
- "</username>" .
- "<id>" .
+ $this->author_list .= "<contributor>" .
+ "<username>" .
+ htmlentities( $row->rev_user_text ) .
+ "</username>" .
+ "<id>" .
$row->rev_user .
- "</id>" .
+ "</id>" .
"</contributor>";
}
wfProfileOut( $fname );
@@ -253,7 +257,7 @@ class WikiExporter {
* separate database connection not managed by LoadBalancer; some
* blob storage types will make queries to pull source data.
*
- * @param ResultWrapper $resultset
+ * @param $resultset ResultWrapper
* @access private
*/
function outputStream( $resultset ) {
@@ -263,7 +267,11 @@ class WikiExporter {
$last->page_namespace != $row->page_namespace ||
$last->page_title != $row->page_title ) {
if( isset( $last ) ) {
- $output = $this->writer->closePage();
+ $output = '';
+ if( $this->dumpUploads ) {
+ $output .= $this->writer->writeUploads( $last );
+ }
+ $output .= $this->writer->closePage();
$this->sink->writeClosePage( $output );
}
$output = $this->writer->openPage( $row );
@@ -274,7 +282,12 @@ class WikiExporter {
$this->sink->writeRevision( $row, $output );
}
if( isset( $last ) ) {
- $output = $this->author_list . $this->writer->closePage();
+ $output = '';
+ if( $this->dumpUploads ) {
+ $output .= $this->writer->writeUploads( $last );
+ }
+ $output .= $this->author_list;
+ $output .= $this->writer->closePage();
$this->sink->writeClosePage( $output );
}
$resultset->free();
@@ -282,7 +295,7 @@ class WikiExporter {
}
/**
- * @addtogroup Dump
+ * @ingroup Dump
*/
class XmlDumpWriter {
@@ -375,7 +388,7 @@ class XmlDumpWriter {
* Opens a <page> section on the output stream, with data
* from the given database row.
*
- * @param object $row
+ * @param $row object
* @return string
* @access private
*/
@@ -404,7 +417,7 @@ class XmlDumpWriter {
* Dumps a <revision> section on the output stream, with
* data filled in from the given database row.
*
- * @param object $row
+ * @param $row object
* @return string
* @access private
*/
@@ -415,20 +428,12 @@ class XmlDumpWriter {
$out = " <revision>\n";
$out .= " " . wfElement( 'id', null, strval( $row->rev_id ) ) . "\n";
- $ts = wfTimestamp( TS_ISO_8601, $row->rev_timestamp );
- $out .= " " . wfElement( 'timestamp', null, $ts ) . "\n";
+ $out .= $this->writeTimestamp( $row->rev_timestamp );
if( $row->rev_deleted & Revision::DELETED_USER ) {
$out .= " " . wfElement( 'contributor', array( 'deleted' => 'deleted' ) ) . "\n";
} else {
- $out .= " <contributor>\n";
- if( $row->rev_user ) {
- $out .= " " . wfElementClean( 'username', null, strval( $row->rev_user_text ) ) . "\n";
- $out .= " " . wfElement( 'id', null, strval( $row->rev_user ) ) . "\n";
- } else {
- $out .= " " . wfElementClean( 'ip', null, strval( $row->rev_user_text ) ) . "\n";
- }
- $out .= " </contributor>\n";
+ $out .= $this->writeContributor( $row->rev_user, $row->rev_user_text );
}
if( $row->rev_minor_edit ) {
@@ -461,12 +466,58 @@ class XmlDumpWriter {
return $out;
}
+ function writeTimestamp( $timestamp ) {
+ $ts = wfTimestamp( TS_ISO_8601, $timestamp );
+ return " " . wfElement( '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";
+ } else {
+ $out .= " " . wfElementClean( 'ip', null, strval( $text ) ) . "\n";
+ }
+ $out .= " </contributor>\n";
+ return $out;
+ }
+
+ /**
+ * Warning! This data is potentially inconsistent. :(
+ */
+ function writeUploads( $row ) {
+ if( $row->page_namespace == NS_IMAGE ) {
+ $img = wfFindFile( $row->page_title );
+ if( $img ) {
+ $out = '';
+ foreach( array_reverse( $img->getHistory() ) as $ver ) {
+ $out .= $this->writeUpload( $ver );
+ }
+ $out .= $this->writeUpload( $img );
+ return $out;
+ }
+ }
+ return '';
+ }
+
+ function writeUpload( $file ) {
+ 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" .
+ " </upload>\n";
+ }
+
}
/**
* Base class for output stream; prints to stdout or buffer or whereever.
- * @addtogroup Dump
+ * @ingroup Dump
*/
class DumpOutput {
function writeOpenStream( $string ) {
@@ -500,7 +551,7 @@ class DumpOutput {
/**
* Stream outputter to send data to a file.
- * @addtogroup Dump
+ * @ingroup Dump
*/
class DumpFileOutput extends DumpOutput {
var $handle;
@@ -518,7 +569,7 @@ class DumpFileOutput extends DumpOutput {
* Stream outputter to send data to a file via some filter program.
* Even if compression is available in a library, using a separate
* program can allow us to make use of a multi-processor system.
- * @addtogroup Dump
+ * @ingroup Dump
*/
class DumpPipeOutput extends DumpFileOutput {
function DumpPipeOutput( $command, $file = null ) {
@@ -531,7 +582,7 @@ class DumpPipeOutput extends DumpFileOutput {
/**
* Sends dump output via the gzip compressor.
- * @addtogroup Dump
+ * @ingroup Dump
*/
class DumpGZipOutput extends DumpPipeOutput {
function DumpGZipOutput( $file ) {
@@ -541,7 +592,7 @@ class DumpGZipOutput extends DumpPipeOutput {
/**
* Sends dump output via the bgzip2 compressor.
- * @addtogroup Dump
+ * @ingroup Dump
*/
class DumpBZip2Output extends DumpPipeOutput {
function DumpBZip2Output( $file ) {
@@ -551,7 +602,7 @@ class DumpBZip2Output extends DumpPipeOutput {
/**
* Sends dump output via the p7zip compressor.
- * @addtogroup Dump
+ * @ingroup Dump
*/
class Dump7ZipOutput extends DumpPipeOutput {
function Dump7ZipOutput( $file ) {
@@ -569,7 +620,7 @@ class Dump7ZipOutput extends DumpPipeOutput {
* Dump output filter class.
* This just does output filtering and streaming; XML formatting is done
* higher up, so be careful in what you do.
- * @addtogroup Dump
+ * @ingroup Dump
*/
class DumpFilter {
function DumpFilter( &$sink ) {
@@ -615,17 +666,17 @@ class DumpFilter {
/**
* Simple dump output filter to exclude all talk pages.
- * @addtogroup Dump
+ * @ingroup Dump
*/
class DumpNotalkFilter extends DumpFilter {
function pass( $page ) {
- return !Namespace::isTalk( $page->page_namespace );
+ return !MWNamespace::isTalk( $page->page_namespace );
}
}
/**
* Dump output filter to include or exclude pages in a given set of namespaces.
- * @addtogroup Dump
+ * @ingroup Dump
*/
class DumpNamespaceFilter extends DumpFilter {
var $invert = false;
@@ -680,7 +731,7 @@ class DumpNamespaceFilter extends DumpFilter {
/**
* Dump output filter to include only the last revision in each page sequence.
- * @addtogroup Dump
+ * @ingroup Dump
*/
class DumpLatestFilter extends DumpFilter {
var $page, $pageString, $rev, $revString;
@@ -712,7 +763,7 @@ class DumpLatestFilter extends DumpFilter {
/**
* Base class for output stream; prints to stdout or buffer or whereever.
- * @addtogroup Dump
+ * @ingroup Dump
*/
class DumpMultiWriter {
function DumpMultiWriter( $sinks ) {
@@ -766,5 +817,3 @@ function xmlsafe( $string ) {
wfProfileOut( $fname );
return $string;
}
-
-
diff --git a/includes/ExternalEdit.php b/includes/ExternalEdit.php
index f5ce5b9d..1c58f442 100644
--- a/includes/ExternalEdit.php
+++ b/includes/ExternalEdit.php
@@ -6,8 +6,6 @@
*/
/**
- *
- *
* Support for external editors to modify both text and files
* in external applications. It works as follows: MediaWiki
* sends a meta-file with the MIME type 'application/x-external-editor'
@@ -68,4 +66,3 @@ CONTROL;
echo $control;
}
}
-
diff --git a/includes/ExternalStore.php b/includes/ExternalStore.php
index 79937b85..e2b78566 100644
--- a/includes/ExternalStore.php
+++ b/includes/ExternalStore.php
@@ -1,49 +1,54 @@
<?php
/**
+ * @defgroup ExternalStorage ExternalStorage
+ */
+
+/**
* Constructor class for data kept in external repositories
*
* External repositories might be populated by maintenance/async
* scripts, thus partial moving of data may be possible, as well
* as possibility to have any storage format (i.e. for archives)
+ *
+ * @ingroup ExternalStorage
*/
-
class ExternalStore {
/* Fetch data from given URL */
static function fetchFromURL($url) {
global $wgExternalStores;
- if (!$wgExternalStores)
+ if( !$wgExternalStores )
return false;
- @list($proto,$path)=explode('://',$url,2);
+ @list( $proto, $path ) = explode( '://', $url, 2 );
/* Bad URL */
- if ($path=="")
+ if( $path == '' )
return false;
- $store =& ExternalStore::getStoreObject( $proto );
+ $store = self::getStoreObject( $proto );
if ( $store === false )
return false;
- return $store->fetchFromURL($url);
+ return $store->fetchFromURL( $url );
}
/**
* Get an external store object of the given type
*/
- static function &getStoreObject( $proto ) {
+ static function getStoreObject( $proto ) {
global $wgExternalStores;
- if (!$wgExternalStores)
+ if( !$wgExternalStores )
return false;
/* Protocol not enabled */
- if (!in_array( $proto, $wgExternalStores ))
+ if( !in_array( $proto, $wgExternalStores ) )
return false;
- $class='ExternalStore'.ucfirst($proto);
+ $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;
}
- $store=new $class();
- return $store;
+
+ return new $class();
}
/**
@@ -54,7 +59,7 @@ class ExternalStore {
*/
static function insert( $url, $data ) {
list( $proto, $params ) = explode( '://', $url, 2 );
- $store =& ExternalStore::getStoreObject( $proto );
+ $store = self::getStoreObject( $proto );
if ( $store === false ) {
return false;
} else {
@@ -62,4 +67,3 @@ class ExternalStore {
}
}
}
-
diff --git a/includes/ExternalStoreDB.php b/includes/ExternalStoreDB.php
index f9046f74..549412d1 100644
--- a/includes/ExternalStoreDB.php
+++ b/includes/ExternalStoreDB.php
@@ -1,12 +1,4 @@
<?php
-/**
- *
- *
- * DB accessable external objects
- *
- */
-
-
/**
* External database storage will use one (or more) separate connection pools
@@ -28,16 +20,15 @@ $wgExternalLoadBalancers = array();
global $wgExternalBlobCache;
$wgExternalBlobCache = array();
+/**
+ * DB accessable external objects
+ * @ingroup ExternalStorage
+ */
class ExternalStoreDB {
/** @todo Document.*/
function &getLoadBalancer( $cluster ) {
- global $wgExternalServers, $wgExternalLoadBalancers;
- if ( !array_key_exists( $cluster, $wgExternalLoadBalancers ) ) {
- $wgExternalLoadBalancers[$cluster] = LoadBalancer::newFromParams( $wgExternalServers[$cluster] );
- }
- $wgExternalLoadBalancers[$cluster]->allowLagged(true);
- return $wgExternalLoadBalancers[$cluster];
+ return wfGetLBFactory()->getExternalLB( $cluster );
}
/** @todo Document.*/
@@ -144,4 +135,3 @@ class ExternalStoreDB {
return "DB://$cluster/$id";
}
}
-
diff --git a/includes/ExternalStoreHttp.php b/includes/ExternalStoreHttp.php
index ef907df5..6eb33b39 100644
--- a/includes/ExternalStoreHttp.php
+++ b/includes/ExternalStoreHttp.php
@@ -1,11 +1,9 @@
<?php
/**
- *
- *
* Example class for HTTP accessable external objects
*
+ * @ingroup ExternalStorage
*/
-
class ExternalStoreHttp {
/* Fetch data from given URL */
function fetchFromURL($url) {
@@ -19,4 +17,3 @@ class ExternalStoreHttp {
* whatever, for initial ext storage
*/
}
-
diff --git a/includes/FakeTitle.php b/includes/FakeTitle.php
index b63ae505..4c2eddc8 100644
--- a/includes/FakeTitle.php
+++ b/includes/FakeTitle.php
@@ -83,5 +83,3 @@ class FakeTitle {
function trackbackURL() { $this->error(); }
function trackbackRDF() { $this->error(); }
}
-
-
diff --git a/includes/Feed.php b/includes/Feed.php
index 309b29bd..512057d9 100644
--- a/includes/Feed.php
+++ b/includes/Feed.php
@@ -300,5 +300,3 @@ class AtomFeed extends ChannelFeed {
</feed><?php
}
}
-
-?>
diff --git a/includes/FeedUtils.php b/includes/FeedUtils.php
new file mode 100644
index 00000000..aa784c02
--- /dev/null
+++ b/includes/FeedUtils.php
@@ -0,0 +1,152 @@
+<?php
+
+// TODO: document
+class FeedUtils {
+
+ public static function checkPurge( $timekey, $key ) {
+ global $wgRequest, $wgUser, $messageMemc;
+ $purge = $wgRequest->getVal( 'action' ) === 'purge';
+ if ( $purge && $wgUser->isAllowed('purge') ) {
+ $messageMemc->delete( $timekey );
+ $messageMemc->delete( $key );
+ }
+ }
+
+ public static function checkFeedOutput( $type ) {
+ global $wgFeed, $wgOut, $wgFeedClasses;
+
+ if ( !$wgFeed ) {
+ global $wgOut;
+ $wgOut->addWikiMsg( 'feed-unavailable' );
+ return false;
+ }
+
+ if( !isset( $wgFeedClasses[$type] ) ) {
+ wfHttpError( 500, "Internal Server Error", "Unsupported feed type." );
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Format a diff for the newsfeed
+ */
+ public static function formatDiff( $row ) {
+ global $wgUser;
+
+ $titleObj = Title::makeTitle( $row->rc_namespace, $row->rc_title );
+ $timestamp = wfTimestamp( TS_MW, $row->rc_timestamp );
+ $actiontext = '';
+ if( $row->rc_type == RC_LOG ) {
+ if( $row->rc_deleted & LogPage::DELETED_ACTION ) {
+ $actiontext = wfMsgHtml('rev-deleted-event');
+ } else {
+ $actiontext = LogPage::actionText( $row->rc_log_type, $row->rc_log_action,
+ $titleObj, $wgUser->getSkin(), LogPage::extractParams($row->rc_params,true,true) );
+ }
+ }
+ return self::formatDiffRow( $titleObj,
+ $row->rc_last_oldid, $row->rc_this_oldid,
+ $timestamp,
+ ($row->rc_deleted & Revision::DELETED_COMMENT) ? wfMsgHtml('rev-deleted-comment') : $row->rc_comment,
+ $actiontext );
+ }
+
+ public static function formatDiffRow( $title, $oldid, $newid, $timestamp, $comment, $actiontext='' ) {
+ global $wgFeedDiffCutoff, $wgContLang, $wgUser;
+ wfProfileIn( __FUNCTION__ );
+
+ $skin = $wgUser->getSkin();
+ # log enties
+ $completeText = '<p>' . implode( ' ',
+ array_filter(
+ array(
+ $actiontext,
+ $skin->formatComment( $comment ) ) ) ) . "</p>\n";
+
+ //NOTE: Check permissions for anonymous users, not current user.
+ // No "privileged" version should end up in the cache.
+ // Most feed readers will not log in anway.
+ $anon = new User();
+ $accErrors = $title->getUserPermissionsErrors( 'read', $anon, true );
+
+ if( $title->getNamespace() >= 0 && !$accErrors ) {
+ 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 ) ) );
+
+
+ if ( strlen( $diffText ) > $wgFeedDiffCutoff ) {
+ // Omit large diffs
+ $diffLink = $title->escapeFullUrl(
+ 'diff=' . $newid .
+ '&oldid=' . $oldid );
+ $diffText = '<a href="' .
+ $diffLink .
+ '">' .
+ htmlspecialchars( wfMsgForContent( 'showdiff' ) ) .
+ '</a>';
+ } elseif ( $diffText === false ) {
+ // Error in diff engine, probably a missing revision
+ $diffText = "<p>Can't load revision $newid</p>";
+ } else {
+ // Diff output fine, clean up any illegal UTF-8
+ $diffText = UtfNormal::cleanUp( $diffText );
+ $diffText = self::applyDiffStyle( $diffText );
+ }
+ wfProfileOut( __FUNCTION__."-dodiff" );
+ } else {
+ $rev = Revision::newFromId( $newid );
+ if( is_null( $rev ) ) {
+ $newtext = '';
+ } else {
+ $newtext = $rev->getText();
+ }
+ $diffText = '<p><b>' . wfMsg( 'newpage' ) . '</b></p>' .
+ '<div>' . nl2br( htmlspecialchars( $newtext ) ) . '</div>';
+ }
+ $completeText .= $diffText;
+ }
+
+ wfProfileOut( __FUNCTION__ );
+ return $completeText;
+ }
+
+ /**
+ * Hacky application of diff styles for the feeds.
+ * Might be 'cleaner' to use DOM or XSLT or something,
+ * but *gack* it's a pain in the ass.
+ *
+ * @param $text String:
+ * @return string
+ * @private
+ */
+ public static function applyDiffStyle( $text ) {
+ $styles = array(
+ 'diff' => 'background-color: white; color:black;',
+ 'diff-otitle' => 'background-color: white; color:black;',
+ 'diff-ntitle' => 'background-color: white; color:black;',
+ 'diff-addedline' => 'background: #cfc; color:black; font-size: smaller;',
+ 'diff-deletedline' => 'background: #ffa; color:black; font-size: smaller;',
+ 'diff-context' => 'background: #eee; color:black; font-size: smaller;',
+ 'diffchange' => 'color: red; font-weight: bold; text-decoration: none;',
+ );
+
+ foreach( $styles as $class => $style ) {
+ $text = preg_replace( "/(<[^>]+)class=(['\"])$class\\2([^>]*>)/",
+ "\\1style=\"$style\"\\3", $text );
+ }
+
+ return $text;
+ }
+
+} \ No newline at end of file
diff --git a/includes/FileDeleteForm.php b/includes/FileDeleteForm.php
index 71e2c1ae..bc80c2b2 100644
--- a/includes/FileDeleteForm.php
+++ b/includes/FileDeleteForm.php
@@ -3,7 +3,7 @@
/**
* File deletion user interface
*
- * @addtogroup Media
+ * @ingroup Media
* @author Rob Church <robchur@gmail.com>
*/
class FileDeleteForm {
@@ -13,7 +13,7 @@ class FileDeleteForm {
private $oldfile = null;
private $oldimage = '';
-
+
/**
* Constructor
*
@@ -23,7 +23,7 @@ class FileDeleteForm {
$this->title = $file->getTitle();
$this->file = $file;
}
-
+
/**
* Fulfil the request; shows the form or deletes the file,
* pending authentication, confirmation, etc.
@@ -35,32 +35,31 @@ class FileDeleteForm {
if( wfReadOnly() ) {
$wgOut->readOnlyPage();
return;
- } elseif( !$wgUser->isLoggedIn() ) {
- $wgOut->showErrorPage( 'uploadnologin', 'uploadnologintext' );
- return;
- } elseif( !$wgUser->isAllowed( 'delete' ) ) {
- $wgOut->permissionRequired( 'delete' );
- return;
- } elseif( $wgUser->isBlocked() ) {
- $wgOut->blockedPage();
+ }
+ $permission_errors = $this->title->getUserPermissionsErrors('delete', $wgUser);
+ if (count($permission_errors)>0) {
+ $wgOut->showPermissionsErrorPage( $permission_errors );
return;
}
-
+
$this->oldimage = $wgRequest->getText( 'oldimage', false );
$token = $wgRequest->getText( 'wpEditToken' );
- if( $this->oldimage && !$this->isValidOldSpec() ) {
+ # Flag to hide all contents of the archived revisions
+ $suppress = $wgRequest->getVal( 'wpSuppress' ) && $wgUser->isAllowed('suppressrevision');
+
+ if( $this->oldimage && !self::isValidOldSpec($this->oldimage) ) {
$wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars( $this->oldimage ) );
return;
}
if( $this->oldimage )
$this->oldfile = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $this->title, $this->oldimage );
-
- if( !$this->haveDeletableFile() ) {
+
+ if( !self::haveDeletableFile($this->file, $this->oldfile, $this->oldimage) ) {
$wgOut->addHtml( $this->prepareMessage( 'filedelete-nofile' ) );
$wgOut->addReturnTo( $this->title );
return;
}
-
+
// Perform the deletion if appropriate
if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $token, $this->oldimage ) ) {
$this->DeleteReasonList = $wgRequest->getText( 'wpDeleteReasonList' );
@@ -72,24 +71,9 @@ class FileDeleteForm {
} elseif ( $reason == 'other' ) {
$reason = $this->DeleteReason;
}
- if( $this->oldimage ) {
- $status = $this->file->deleteOld( $this->oldimage, $reason );
- if( $status->ok ) {
- // Need to do a log item
- $log = new LogPage( 'delete' );
- $logComment = wfMsgForContent( 'deletedrevision', $this->oldimage );
- if( trim( $reason ) != '' )
- $logComment .= ": {$reason}";
- $log->addEntry( 'delete', $this->title, $logComment );
- }
- } else {
- $status = $this->file->delete( $reason );
- if( $status->ok ) {
- // Need to delete the associated article
- $article = new Article( $this->title );
- $article->doDeleteArticle( $reason );
- }
- }
+
+ $status = self::doDelete( $this->title, $this->file, $this->oldimage, $reason, $suppress );
+
if( !$status->isGood() )
$wgOut->addWikiText( $status->getWikiText( 'filedeleteerror-short', 'filedeleteerror-long' ) );
if( $status->ok ) {
@@ -101,11 +85,40 @@ class FileDeleteForm {
}
return;
}
-
+
$this->showForm();
$this->showLogEntries();
}
+ public static function doDelete( &$title, &$file, &$oldimage, $reason, $suppress ) {
+ $article = null;
+ if( $oldimage ) {
+ $status = $file->deleteOld( $oldimage, $reason, $suppress );
+ if( $status->ok ) {
+ // Need to do a log item
+ $log = new LogPage( 'delete' );
+ $logComment = wfMsgForContent( 'deletedrevision', $oldimage );
+ if( trim( $reason ) != '' )
+ $logComment .= ": {$reason}";
+ $log->addEntry( 'delete', $title, $logComment );
+ }
+ } else {
+ $status = $file->delete( $reason, $suppress );
+ if( $status->ok ) {
+ // 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( $status->isGood() ) wfRunHooks('FileDeleteComplete', array(
+ &$file, &$oldimage, &$article, &$wgUser, &$reason));
+
+ return $status;
+ }
+
/**
* Show the confirmation form
*/
@@ -113,6 +126,14 @@ class FileDeleteForm {
global $wgOut, $wgUser, $wgRequest, $wgContLang;
$align = $wgContLang->isRtl() ? 'left' : 'right';
+ 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>";
+ } else {
+ $suppress = '';
+ }
+
$form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getAction() ) ) .
Xml::openElement( 'fieldset' ) .
Xml::element( 'legend', null, wfMsg( 'filedelete-legend' ) ) .
@@ -125,7 +146,7 @@ class FileDeleteForm {
"</td>
<td>" .
Xml::listDropDown( 'wpDeleteReasonList',
- wfMsgForContent( 'filedelete-reason-dropdown' ),
+ wfMsgForContent( 'filedelete-reason-dropdown' ),
wfMsgForContent( 'filedelete-reason-otherlist' ), '', 'wpReasonDropDown', 1 ) .
"</td>
</tr>
@@ -137,6 +158,7 @@ class FileDeleteForm {
Xml::input( 'wpReason', 60, $wgRequest->getText( 'wpReason' ), array( 'type' => 'text', 'maxlength' => '255', 'tabindex' => '2', 'id' => 'wpReason' ) ) .
"</td>
</tr>
+ {$suppress}
<tr>
<td></td>
<td>" .
@@ -147,6 +169,12 @@ class FileDeleteForm {
Xml::closeElement( 'fieldset' ) .
Xml::closeElement( 'form' );
+ if ( $wgUser->isAllowed( 'editinterface' ) ) {
+ $skin = $wgUser->getSkin();
+ $link = $skin->makeLink ( 'MediaWiki:Filedelete-reason-dropdown', wfMsgHtml( 'filedelete-edit-reasonlist' ) );
+ $form .= '<p class="mw-filedelete-editreasons">' . $link . '</p>';
+ }
+
$wgOut->addHtml( $form );
}
@@ -156,19 +184,9 @@ class FileDeleteForm {
private function showLogEntries() {
global $wgOut;
$wgOut->addHtml( '<h2>' . htmlspecialchars( LogPage::logName( 'delete' ) ) . "</h2>\n" );
- $reader = new LogViewer(
- new LogReader(
- new FauxRequest(
- array(
- 'type' => 'delete',
- 'page' => $this->title->getPrefixedText(),
- )
- )
- )
- );
- $reader->showList( $wgOut );
+ LogEventsList::showLogExtract( $wgOut, 'delete', $this->title->getPrefixedText() );
}
-
+
/**
* Prepare a message referring to the file being deleted,
* showing an appropriate message depending upon whether
@@ -196,7 +214,7 @@ class FileDeleteForm {
);
}
}
-
+
/**
* Set headers, titles and other bits
*/
@@ -206,18 +224,18 @@ class FileDeleteForm {
$wgOut->setRobotPolicy( 'noindex,nofollow' );
$wgOut->setSubtitle( wfMsg( 'filedelete-backlink', $wgUser->getSkin()->makeKnownLinkObj( $this->title ) ) );
}
-
+
/**
* Is the provided `oldimage` value valid?
*
* @return bool
*/
- private function isValidOldSpec() {
- return strlen( $this->oldimage ) >= 16
- && strpos( $this->oldimage, '/' ) === false
- && strpos( $this->oldimage, '\\' ) === false;
+ public static function isValidOldSpec($oldimage) {
+ return strlen( $oldimage ) >= 16
+ && strpos( $oldimage, '/' ) === false
+ && strpos( $oldimage, '\\' ) === false;
}
-
+
/**
* Could we delete the file specified? If an `oldimage`
* value was provided, does it correspond to an
@@ -225,12 +243,12 @@ class FileDeleteForm {
*
* @return bool
*/
- private function haveDeletableFile() {
- return $this->oldimage
- ? $this->oldfile && $this->oldfile->exists() && $this->oldfile->isLocal()
- : $this->file && $this->file->exists() && $this->file->isLocal();
+ public static function haveDeletableFile(&$file, &$oldfile, $oldimage) {
+ return $oldimage
+ ? $oldfile && $oldfile->exists() && $oldfile->isLocal()
+ : $file && $file->exists() && $file->isLocal();
}
-
+
/**
* Prepare the form action
*
@@ -243,7 +261,7 @@ class FileDeleteForm {
$q[] = 'oldimage=' . urlencode( $this->oldimage );
return $this->title->getLocalUrl( implode( '&', $q ) );
}
-
+
/**
* Extract the timestamp of the old version
*
@@ -252,5 +270,5 @@ class FileDeleteForm {
private function getTimestamp() {
return $this->oldfile->getTimestamp();
}
-
+
}
diff --git a/includes/FileRevertForm.php b/includes/FileRevertForm.php
index f335d024..385d83bc 100644
--- a/includes/FileRevertForm.php
+++ b/includes/FileRevertForm.php
@@ -3,16 +3,17 @@
/**
* File reversion user interface
*
- * @addtogroup Media
+ * @ingroup Media
* @author Rob Church <robchur@gmail.com>
*/
class FileRevertForm {
- private $title = null;
- private $file = null;
- private $oldimage = '';
- private $timestamp = false;
-
+ protected $title = null;
+ protected $file = null;
+ protected $archiveName = '';
+ protected $timestamp = false;
+ protected $oldFile;
+
/**
* Constructor
*
@@ -22,7 +23,7 @@ class FileRevertForm {
$this->title = $file->getTitle();
$this->file = $file;
}
-
+
/**
* Fulfil the request; shows the form or reverts the file,
* pending authentication, confirmation, etc.
@@ -37,7 +38,7 @@ class FileRevertForm {
} elseif( !$wgUser->isLoggedIn() ) {
$wgOut->showErrorPage( 'uploadnologin', 'uploadnologintext' );
return;
- } elseif( !$this->title->userCan( 'edit' ) ) {
+ } elseif( !$this->title->userCan( 'edit' ) || !$this->title->userCan( 'upload' ) ) {
// The standard read-only thing doesn't make a whole lot of sense
// here; surely it should show the image or something? -- RC
$article = new Article( $this->title );
@@ -47,23 +48,23 @@ class FileRevertForm {
$wgOut->blockedPage();
return;
}
-
- $this->oldimage = $wgRequest->getText( 'oldimage' );
+
+ $this->archiveName = $wgRequest->getText( 'oldimage' );
$token = $wgRequest->getText( 'wpEditToken' );
if( !$this->isValidOldSpec() ) {
- $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars( $this->oldimage ) );
+ $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars( $this->archiveName ) );
return;
}
-
+
if( !$this->haveOldVersion() ) {
$wgOut->addHtml( wfMsgExt( 'filerevert-badversion', 'parse' ) );
$wgOut->returnToMain( false, $this->title );
return;
}
-
+
// Perform the reversion if appropriate
- if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $token, $this->oldimage ) ) {
- $source = $this->file->getArchiveVirtualUrl( $this->oldimage );
+ if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $token, $this->archiveName ) ) {
+ $source = $this->file->getArchiveVirtualUrl( $this->archiveName );
$comment = $wgRequest->getText( 'wpComment' );
// TODO: Preserve file properties from database instead of reloading from file
$status = $this->file->upload( $source, $comment, $comment );
@@ -71,96 +72,100 @@ class FileRevertForm {
$wgOut->addHtml( wfMsgExt( 'filerevert-success', 'parse', $this->title->getText(),
$wgLang->date( $this->getTimestamp(), true ),
$wgLang->time( $this->getTimestamp(), true ),
- wfExpandUrl( $this->file->getArchiveUrl( $this->oldimage ) ) ) );
+ wfExpandUrl( $this->file->getArchiveUrl( $this->archiveName ) ) ) );
$wgOut->returnToMain( false, $this->title );
} else {
$wgOut->addWikiText( $status->getWikiText() );
}
return;
}
-
+
// Show the form
- $this->showForm();
+ $this->showForm();
}
-
+
/**
* Show the confirmation form
*/
- private function showForm() {
+ protected function showForm() {
global $wgOut, $wgUser, $wgRequest, $wgLang, $wgContLang;
$timestamp = $this->getTimestamp();
$form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getAction() ) );
- $form .= Xml::hidden( 'wpEditToken', $wgUser->editToken( $this->oldimage ) );
+ $form .= Xml::hidden( 'wpEditToken', $wgUser->editToken( $this->archiveName ) );
$form .= '<fieldset><legend>' . wfMsgHtml( 'filerevert-legend' ) . '</legend>';
$form .= wfMsgExt( 'filerevert-intro', 'parse', $this->title->getText(),
$wgLang->date( $timestamp, true ), $wgLang->time( $timestamp, true ),
- wfExpandUrl( $this->file->getArchiveUrl( $this->oldimage ) ) );
+ wfExpandUrl( $this->file->getArchiveUrl( $this->archiveName ) ) );
$form .= '<p>' . Xml::inputLabel( wfMsg( 'filerevert-comment' ), 'wpComment', 'wpComment',
60, wfMsgForContent( 'filerevert-defaultcomment',
$wgContLang->date( $timestamp, false, false ), $wgContLang->time( $timestamp, false, false ) ) ) . '</p>';
$form .= '<p>' . Xml::submitButton( wfMsg( 'filerevert-submit' ) ) . '</p>';
$form .= '</fieldset>';
$form .= '</form>';
-
+
$wgOut->addHtml( $form );
}
-
+
/**
* Set headers, titles and other bits
*/
- private function setHeaders() {
+ protected function setHeaders() {
global $wgOut, $wgUser;
$wgOut->setPageTitle( wfMsg( 'filerevert', $this->title->getText() ) );
$wgOut->setRobotPolicy( 'noindex,nofollow' );
$wgOut->setSubtitle( wfMsg( 'filerevert-backlink', $wgUser->getSkin()->makeKnownLinkObj( $this->title ) ) );
}
-
+
/**
* Is the provided `oldimage` value valid?
*
* @return bool
*/
- private function isValidOldSpec() {
- return strlen( $this->oldimage ) >= 16
- && strpos( $this->oldimage, '/' ) === false
- && strpos( $this->oldimage, '\\' ) === false;
+ protected function isValidOldSpec() {
+ return strlen( $this->archiveName ) >= 16
+ && strpos( $this->archiveName, '/' ) === false
+ && strpos( $this->archiveName, '\\' ) === false;
}
-
+
/**
* Does the provided `oldimage` value correspond
* to an existing, local, old version of this file?
*
* @return bool
*/
- private function haveOldVersion() {
- $file = wfFindFile( $this->title, $this->oldimage );
- return $file && $file->exists() && $file->isLocal();
+ protected function haveOldVersion() {
+ return $this->getOldFile()->exists();
}
-
+
/**
* Prepare the form action
*
* @return string
*/
- private function getAction() {
+ protected function getAction() {
$q = array();
$q[] = 'action=revert';
- $q[] = 'oldimage=' . urlencode( $this->oldimage );
+ $q[] = 'oldimage=' . urlencode( $this->archiveName );
return $this->title->getLocalUrl( implode( '&', $q ) );
}
-
+
/**
* Extract the timestamp of the old version
*
* @return string
*/
- private function getTimestamp() {
+ protected function getTimestamp() {
if( $this->timestamp === false ) {
- $file = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $this->title, $this->oldimage );
- $this->timestamp = $file->getTimestamp();
+ $this->timestamp = $this->getOldFile()->getTimestamp();
}
return $this->timestamp;
}
-
-} \ No newline at end of file
+
+ protected function getOldFile() {
+ if ( !isset( $this->oldFile ) ) {
+ $this->oldFile = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $this->title, $this->archiveName );
+ }
+ return $this->oldFile;
+ }
+}
diff --git a/includes/FileStore.php b/includes/FileStore.php
index a547e7e4..c01350c0 100644
--- a/includes/FileStore.php
+++ b/includes/FileStore.php
@@ -5,13 +5,13 @@
*/
class FileStore {
const DELETE_ORIGINAL = 1;
-
+
/**
* Fetch the FileStore object for a given storage group
*/
static function get( $group ) {
global $wgFileStore;
-
+
if( isset( $wgFileStore[$group] ) ) {
$info = $wgFileStore[$group];
return new FileStore( $group,
@@ -22,14 +22,14 @@ class FileStore {
return null;
}
}
-
+
private function __construct( $group, $directory, $path, $hash ) {
$this->mGroup = $group;
$this->mDirectory = $directory;
$this->mPath = $path;
$this->mHashLevel = $hash;
}
-
+
/**
* Acquire a lock; use when performing write operations on a store.
* This is attached to your master database connection, so if you
@@ -47,7 +47,7 @@ class FileStore {
$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 {
@@ -55,7 +55,7 @@ class FileStore {
return false;
}
}
-
+
/**
* Release the global file store lock.
*/
@@ -69,11 +69,11 @@ class FileStore {
$dbw->fetchObject( $result );
$dbw->freeResult( $result );
}
-
+
private static function lockName() {
return 'MediaWiki.' . wfWikiID() . '.FileStore';
}
-
+
/**
* Copy a file into the file store from elsewhere in the filesystem.
* Should be protected by FileStore::lock() to avoid race conditions.
@@ -89,7 +89,7 @@ class FileStore {
$destPath = $this->filePath( $key );
return $this->copyFile( $sourcePath, $destPath, $flags );
}
-
+
/**
* Copy a file from the file store to elsewhere in the filesystem.
* Should be protected by FileStore::lock() to avoid race conditions.
@@ -105,19 +105,19 @@ class FileStore {
$sourcePath = $this->filePath( $key );
return $this->copyFile( $sourcePath, $destPath, $flags );
}
-
+
private function copyFile( $sourcePath, $destPath, $flags=0 ) {
if( !file_exists( $sourcePath ) ) {
// Abort! Abort!
throw new FSException( "missing source file '$sourcePath'" );
}
-
+
$transaction = new FSTransaction();
-
+
if( $flags & self::DELETE_ORIGINAL ) {
$transaction->addCommit( FSTransaction::DELETE_FILE, $sourcePath );
}
-
+
if( file_exists( $destPath ) ) {
// An identical file is already present; no need to copy.
} else {
@@ -125,17 +125,17 @@ class FileStore {
wfSuppressWarnings();
$ok = mkdir( dirname( $destPath ), 0777, true );
wfRestoreWarnings();
-
+
if( !$ok ) {
throw new FSException(
"failed to create directory for '$destPath'" );
}
}
-
+
wfSuppressWarnings();
$ok = copy( $sourcePath, $destPath );
wfRestoreWarnings();
-
+
if( $ok ) {
wfDebug( __METHOD__." copied '$sourcePath' to '$destPath'\n" );
$transaction->addRollback( FSTransaction::DELETE_FILE, $destPath );
@@ -144,10 +144,10 @@ class FileStore {
__METHOD__." failed to copy '$sourcePath' to '$destPath'" );
}
}
-
+
return $transaction;
}
-
+
/**
* Delete a file from the file store.
* Caller's responsibility to make sure it's not being used by another row.
@@ -167,7 +167,7 @@ class FileStore {
return FileStore::deleteFile( $destPath );
}
}
-
+
/**
* Delete a non-managed file on a transactional basis.
*
@@ -189,7 +189,7 @@ class FileStore {
throw new FSException( "cannot delete missing file '$path'" );
}
}
-
+
/**
* Stream a contained file directly to HTTP output.
* Will throw a 404 if file is missing; 400 if invalid key.
@@ -201,12 +201,12 @@ class FileStore {
wfHttpError( 400, "Bad request", "Invalid or badly-formed filename." );
return false;
}
-
+
if( file_exists( $path ) ) {
// Set the filename for more convenient save behavior from browsers
// FIXME: Is this safe?
header( 'Content-Disposition: inline; filename="' . $key . '"' );
-
+
require_once 'StreamFile.php';
wfStreamFile( $path );
} else {
@@ -214,7 +214,7 @@ class FileStore {
"The requested resource does not exist." );
}
}
-
+
/**
* Confirm that the given file key is valid.
* Note that a valid key may refer to a file that does not exist.
@@ -229,8 +229,8 @@ class FileStore {
static function validKey( $key ) {
return preg_match( '/^[0-9a-z]{31,32}(\.[0-9a-z]{1,31})?$/', $key );
}
-
-
+
+
/**
* Calculate file storage key from a file on disk.
* You must pass an extension to it, as some files may be calculated
@@ -248,14 +248,14 @@ class FileStore {
wfDebug( __METHOD__.": couldn't hash file '$path'\n" );
return false;
}
-
+
$base36 = wfBaseConvert( $hash, 16, 36, 31 );
if( $extension == '' ) {
$key = $base36;
} else {
$key = $base36 . '.' . $extension;
}
-
+
// Sanity check
if( self::validKey( $key ) ) {
return $key;
@@ -264,7 +264,7 @@ class FileStore {
return false;
}
}
-
+
/**
* Return filesystem path to the given file.
* Note that the file may or may not exist.
@@ -278,7 +278,7 @@ class FileStore {
return false;
}
}
-
+
/**
* Return URL path to the given file, if the store is public.
* @return string or false if not public
@@ -290,7 +290,7 @@ class FileStore {
return false;
}
}
-
+
private function hashPath( $key, $separator ) {
$parts = array();
for( $i = 0; $i < $this->mHashLevel; $i++ ) {
@@ -310,7 +310,7 @@ class FileStore {
*/
class FSTransaction {
const DELETE_FILE = 1;
-
+
/**
* Combine more items into a fancier transaction
*/
@@ -320,7 +320,7 @@ class FSTransaction {
$this->mOnRollback = array_merge(
$this->mOnRollback, $transaction->mOnRollback );
}
-
+
/**
* Perform final actions for success.
* @return true if actions applied ok, false if errors
@@ -328,7 +328,7 @@ class FSTransaction {
function commit() {
return $this->apply( $this->mOnCommit );
}
-
+
/**
* Perform final actions for failure.
* @return true if actions applied ok, false if errors
@@ -336,22 +336,22 @@ class FSTransaction {
function rollback() {
return $this->apply( $this->mOnRollback );
}
-
+
// --- Private and friend functions below...
-
+
function __construct() {
$this->mOnCommit = array();
$this->mOnRollback = array();
}
-
+
function addCommit( $action, $path ) {
$this->mOnCommit[] = array( $action, $path );
}
-
+
function addRollback( $action, $path ) {
$this->mOnRollback[] = array( $action, $path );
}
-
+
private function apply( $actions ) {
$result = true;
foreach( $actions as $item ) {
@@ -372,8 +372,6 @@ class FSTransaction {
}
/**
- * @addtogroup Exception
+ * @ingroup Exception
*/
class FSException extends MWException { }
-
-
diff --git a/includes/FormOptions.php b/includes/FormOptions.php
new file mode 100644
index 00000000..5888a0c4
--- /dev/null
+++ b/includes/FormOptions.php
@@ -0,0 +1,202 @@
+<?php
+/**
+ * Helper class to keep track of options when mixing links and form elements.
+ *
+ * @author Niklas Laxström
+ * @copyright Copyright © 2008, Niklas Laxström
+ */
+
+class FormOptions implements ArrayAccess {
+ const AUTO = -1; //! Automatically detects simple data types
+ const STRING = 0;
+ const INT = 1;
+ const BOOL = 2;
+ const INTNULL = 3; //! Useful for namespace selector
+
+ protected $options = array();
+
+ # Setting up
+
+ public function add( $name, $default, $type = self::AUTO ) {
+ $option = array();
+ $option['default'] = $default;
+ $option['value'] = null;
+ $option['consumed'] = false;
+
+ if ( $type !== self::AUTO ) {
+ $option['type'] = $type;
+ } else {
+ $option['type'] = self::guessType( $default );
+ }
+
+ $this->options[$name] = $option;
+ }
+
+ public function delete( $name ) {
+ $this->validateName( $name, true );
+ unset($this->options[$name]);
+ }
+
+ public static function guessType( $data ) {
+ if ( is_bool($data) ) {
+ return self::BOOL;
+ } elseif( is_int($data) ) {
+ return self::INT;
+ } elseif( is_string($data) ) {
+ return self::STRING;
+ } else {
+ throw new MWException( 'Unsupported datatype' );
+ }
+ }
+
+ # Handling values
+
+ public function validateName( $name, $strict = false ) {
+ if ( !isset($this->options[$name]) ) {
+ if ( $strict ) {
+ throw new MWException( "Invalid option $name" );
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public function setValue( $name, $value, $force = false ) {
+ $this->validateName( $name, true );
+ if ( !$force && $value === $this->options[$name]['default'] ) {
+ // null default values as unchanged
+ $this->options[$name]['value'] = null;
+ } else {
+ $this->options[$name]['value'] = $value;
+ }
+ }
+
+ public function getValue( $name ) {
+ $this->validateName( $name, true );
+ return $this->getValueReal( $this->options[$name] );
+ }
+
+ protected function getValueReal( $option ) {
+ if ( $option['value'] !== null ) {
+ return $option['value'];
+ } else {
+ return $option['default'];
+ }
+ }
+
+ public function reset( $name ) {
+ $this->validateName( $name, true );
+ $this->options[$name]['value'] = null;
+ }
+
+ public function consumeValue( $name ) {
+ $this->validateName( $name, true );
+ $this->options[$name]['consumed'] = true;
+ return $this->getValueReal( $this->options[$name] );
+ }
+
+ public function consumeValues( /*Array*/ $names ) {
+ $out = array();
+ foreach ( $names as $name ) {
+ $this->validateName( $name, true );
+ $this->options[$name]['consumed'] = true;
+ $out[] = $this->getValueReal( $this->options[$name] );
+ }
+ return $out;
+ }
+
+ # Validating values
+
+ public function validateIntBounds( $name, $min, $max ) {
+ $this->validateName( $name, true );
+
+ if ( $this->options[$name]['type'] !== self::INT )
+ throw new MWException( "Option $name is not of type int" );
+
+ $value = $this->getValueReal( $this->options[$name] );
+ $value = max( $min, min( $max, $value ) );
+
+ $this->setValue( $name, $value );
+ }
+
+ # Getting the data out for use
+
+ public function getUnconsumedValues( $all = false ) {
+ $values = array();
+ foreach ( $this->options as $name => $data ) {
+ if ( !$data['consumed'] ) {
+ if ( $all || $data['value'] !== null ) {
+ $values[$name] = $this->getValueReal( $data );
+ }
+ }
+ }
+ return $values;
+ }
+
+ public function getChangedValues() {
+ $values = array();
+ foreach ( $this->options as $name => $data ) {
+ if ( $data['value'] !== null ) {
+ $values[$name] = $data['value'];
+ }
+ }
+ return $values;
+ }
+
+ public function getAllValues() {
+ $values = array();
+ foreach ( $this->options as $name => $data ) {
+ $values[$name] = $this->getValueReal( $data );
+ }
+ return $values;
+ }
+
+ # Reading values
+
+ public function fetchValuesFromRequest( WebRequest $r, $values = false ) {
+ if ( !$values ) {
+ $values = array_keys($this->options);
+ }
+
+ foreach ( $values as $name ) {
+ $default = $this->options[$name]['default'];
+ $type = $this->options[$name]['type'];
+
+ switch( $type ) {
+ case self::BOOL:
+ $value = $r->getBool( $name, $default ); break;
+ case self::INT:
+ $value = $r->getInt( $name, $default ); break;
+ case self::STRING:
+ $value = $r->getText( $name, $default ); break;
+ case self::INTNULL:
+ $value = $r->getIntOrNull( $name ); break;
+ default:
+ throw new MWException( 'Unsupported datatype' );
+ }
+
+ if ( $value !== $default && $value !== null ) {
+ $this->options[$name]['value'] = $value;
+ }
+ }
+ }
+
+ /* ArrayAccess methods */
+ public function offsetExists( $name ) {
+ return isset($this->options[$name]);
+ }
+
+ public function offsetGet( $name ) {
+ return $this->getValue( $name );
+ }
+
+ public function offsetSet( $name, $value ) {
+ return $this->setValue( $name, $value );
+ }
+
+ public function offsetUnset( $name ) {
+ return $this->delete( $name );
+ }
+
+}
diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php
index 2b9543b4..f5a2660c 100644
--- a/includes/GlobalFunctions.php
+++ b/includes/GlobalFunctions.php
@@ -87,6 +87,29 @@ if ( !function_exists( 'array_diff_key' ) ) {
}
}
+/**
+ * Like array_diff( $a, $b ) except that it works with two-dimensional arrays.
+ */
+function wfArrayDiff2( $a, $b ) {
+ return array_udiff( $a, $b, 'wfArrayDiff2_cmp' );
+}
+function wfArrayDiff2_cmp( $a, $b ) {
+ if ( !is_array( $a ) ) {
+ return strcmp( $a, $b );
+ } elseif ( count( $a ) !== count( $b ) ) {
+ return count( $a ) < count( $b ) ? -1 : 1;
+ } else {
+ reset( $a );
+ reset( $b );
+ while( ( list( $keyA, $valueA ) = each( $a ) ) && ( list( $keyB, $valueB ) = each( $b ) ) ) {
+ $cmp = strcmp( $valueA, $valueB );
+ if ( $cmp !== 0 ) {
+ return $cmp;
+ }
+ }
+ return 0;
+ }
+}
/**
* Wrapper for clone(), for compatibility with PHP4-friendly extensions.
@@ -153,12 +176,16 @@ function wfDebug( $text, $logonly = false ) {
global $wgOut, $wgDebugLogFile, $wgDebugComments, $wgProfileOnly, $wgDebugRawPage;
static $recursion = 0;
+ static $cache = array(); // Cache of unoutputted messages
+
# Check for raw action using $_GET not $wgRequest, since the latter might not be initialised yet
if ( isset( $_GET['action'] ) && $_GET['action'] == 'raw' && !$wgDebugRawPage ) {
return;
}
if ( $wgDebugComments && !$logonly ) {
+ $cache[] = $text;
+
if ( !isset( $wgOut ) ) {
return;
}
@@ -170,7 +197,10 @@ function wfDebug( $text, $logonly = false ) {
$wgOut->_unstub();
$recursion--;
}
- $wgOut->debug( $text );
+
+ // add the message and possible cached ones to the output
+ array_map( array( $wgOut, 'debug' ), $cache );
+ $cache = array();
}
if ( '' != $wgDebugLogFile && !$wgProfileOnly ) {
# Strip unprintables; they can switch terminal modes when binary data
@@ -232,29 +262,30 @@ function wfErrorLog( $text, $file ) {
*/
function wfLogProfilingData() {
global $wgRequestTime, $wgDebugLogFile, $wgDebugRawPage, $wgRequest;
- global $wgProfiling, $wgUser;
- if ( $wgProfiling ) {
- $now = wfTime();
- $elapsed = $now - $wgRequestTime;
- $prof = wfGetProfilingOutput( $wgRequestTime, $elapsed );
- $forward = '';
- if( !empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) )
- $forward = ' forwarded for ' . $_SERVER['HTTP_X_FORWARDED_FOR'];
- if( !empty( $_SERVER['HTTP_CLIENT_IP'] ) )
- $forward .= ' client IP ' . $_SERVER['HTTP_CLIENT_IP'];
- if( !empty( $_SERVER['HTTP_FROM'] ) )
- $forward .= ' from ' . $_SERVER['HTTP_FROM'];
- if( $forward )
- $forward = "\t(proxied via {$_SERVER['REMOTE_ADDR']}{$forward})";
- // Don't unstub $wgUser at this late stage just for statistics purposes
- if( StubObject::isRealObject($wgUser) && $wgUser->isAnon() )
- $forward .= ' anon';
- $log = sprintf( "%s\t%04.3f\t%s\n",
- gmdate( 'YmdHis' ), $elapsed,
- urldecode( $wgRequest->getRequestURL() . $forward ) );
- if ( '' != $wgDebugLogFile && ( $wgRequest->getVal('action') != 'raw' || $wgDebugRawPage ) ) {
- wfErrorLog( $log . $prof, $wgDebugLogFile );
- }
+ global $wgProfiler, $wgUser;
+ if ( !isset( $wgProfiler ) )
+ return;
+
+ $now = wfTime();
+ $elapsed = $now - $wgRequestTime;
+ $prof = wfGetProfilingOutput( $wgRequestTime, $elapsed );
+ $forward = '';
+ if( !empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) )
+ $forward = ' forwarded for ' . $_SERVER['HTTP_X_FORWARDED_FOR'];
+ if( !empty( $_SERVER['HTTP_CLIENT_IP'] ) )
+ $forward .= ' client IP ' . $_SERVER['HTTP_CLIENT_IP'];
+ if( !empty( $_SERVER['HTTP_FROM'] ) )
+ $forward .= ' from ' . $_SERVER['HTTP_FROM'];
+ if( $forward )
+ $forward = "\t(proxied via {$_SERVER['REMOTE_ADDR']}{$forward})";
+ // Don't unstub $wgUser at this late stage just for statistics purposes
+ if( StubObject::isRealObject($wgUser) && $wgUser->isAnon() )
+ $forward .= ' anon';
+ $log = sprintf( "%s\t%04.3f\t%s\n",
+ gmdate( 'YmdHis' ), $elapsed,
+ urldecode( $wgRequest->getRequestURL() . $forward ) );
+ if ( '' != $wgDebugLogFile && ( $wgRequest->getVal('action') != 'raw' || $wgDebugRawPage ) ) {
+ wfErrorLog( $log . $prof, $wgDebugLogFile );
}
}
@@ -282,6 +313,11 @@ function wfReadOnly() {
return (bool)$wgReadOnly;
}
+function wfReadOnlyReason() {
+ global $wgReadOnly;
+ wfReadOnly();
+ return $wgReadOnly;
+}
/**
* Get a message from anywhere, for the current user language.
@@ -291,9 +327,9 @@ function wfReadOnly() {
*
* @param $key String: lookup key for the message, usually
* defined in languages/Language.php
- *
- * This function also takes extra optional parameters (not
- * shown in the function definition), which can by used to
+ *
+ * This function also takes extra optional parameters (not
+ * shown in the function definition), which can by used to
* insert variable text into the predefined message.
*/
function wfMsg( $key ) {
@@ -415,24 +451,38 @@ function wfMsgWeirdKey ( $key ) {
* Fetch a message string value, but don't replace any keys yet.
* @param string $key
* @param bool $useDB
- * @param bool $forContent
+ * @param string $langcode Code of the language to get the message for, or
+ * behaves as a content language switch if it is a
+ * boolean.
* @return string
* @private
*/
-function wfMsgGetKey( $key, $useDB, $forContent = false, $transform = true ) {
+function wfMsgGetKey( $key, $useDB, $langCode = false, $transform = true ) {
global $wgParser, $wgContLang, $wgMessageCache, $wgLang;
+ wfRunHooks('NormalizeMessageKey', array(&$key, &$useDB, &$langCode, &$transform));
+
# If $wgMessageCache isn't initialised yet, try to return something sensible.
if( is_object( $wgMessageCache ) ) {
- $message = $wgMessageCache->get( $key, $useDB, $forContent );
+ $message = $wgMessageCache->get( $key, $useDB, $langCode );
if ( $transform ) {
$message = $wgMessageCache->transform( $message );
}
} else {
- if( $forContent ) {
+ if( $langCode === true ) {
$lang = &$wgContLang;
- } else {
+ } 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.' );
+ }
}
# MessageCache::get() does this already, Language::getMessage() doesn't
@@ -523,6 +573,8 @@ function wfMsgWikiHtml( $key ) {
* <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.
* Behavior for conflicting options (e.g., parse+parseinline) is undefined.
*/
function wfMsgExt( $key, $options ) {
@@ -536,12 +588,23 @@ function wfMsgExt( $key, $options ) {
$options = array($options);
}
- $forContent = false;
if( in_array('content', $options) ) {
$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';
+ }
+ } else {
+ $forContent = false;
+ $langCode = false;
}
- $string = wfMsgGetKey( $key, /*DB*/true, $forContent, /*Transform*/false );
+ $string = wfMsgGetKey( $key, /*DB*/true, $langCode, /*Transform*/false );
if( !in_array('replaceafter', $options) ) {
$string = wfMsgReplaceArgs( $string, $args );
@@ -585,7 +648,6 @@ function wfMsgExt( $key, $options ) {
* @deprecated Please return control to the caller or throw an exception
*/
function wfAbruptExit( $error = false ){
- global $wgLoadBalancer;
static $called = false;
if ( $called ){
exit( -1 );
@@ -606,7 +668,7 @@ function wfAbruptExit( $error = false ){
wfLogProfilingData();
if ( !$error ) {
- $wgLoadBalancer->closeAll();
+ wfGetLB()->closeAll();
}
exit( -1 );
}
@@ -629,7 +691,7 @@ function wfDie( $msg='' ) {
}
/**
- * Throw a debugging exception. This function previously once exited the process,
+ * Throw a debugging exception. This function previously once exited the process,
* but now throws an exception instead, with similar results.
*
* @param string $msg Message shown when dieing.
@@ -848,7 +910,7 @@ function wfClientAcceptsGzip() {
* @param $deflimit Default limit if none supplied
* @param $optionname Name of a user preference to check against
* @return array
- *
+ *
*/
function wfCheckLimits( $deflimit = 50, $optionname = 'rclimit' ) {
global $wgRequest;
@@ -947,7 +1009,20 @@ function wfArrayToCGI( $array1, $array2 = NULL )
if ( '' != $cgi ) {
$cgi .= '&';
}
- $cgi .= urlencode( $key ) . '=' . urlencode( $value );
+ if(is_array($value))
+ {
+ $firstTime = true;
+ foreach($value as $v)
+ {
+ $cgi .= ($firstTime ? '' : '&') .
+ urlencode( $key . '[]' ) . '=' .
+ urlencode( $v );
+ $firstTime = false;
+ }
+ }
+ else
+ $cgi .= urlencode( $key ) . '=' .
+ urlencode( $value );
}
}
return $cgi;
@@ -1104,6 +1179,68 @@ function wfMerge( $old, $mine, $yours, &$result ){
}
/**
+ * Returns unified plain-text diff of two texts.
+ * Useful for machine processing of diffs.
+ * @param $before string The text before the changes.
+ * @param $after string The text after the changes.
+ * @param $params string Command-line options for the diff command.
+ * @return string Unified diff of $before and $after
+ */
+function wfDiff( $before, $after, $params = '-u' ) {
+ global $wgDiff;
+
+ # This check may also protect against code injection in
+ # case of broken installations.
+ if( !file_exists( $wgDiff ) ){
+ wfDebug( "diff executable not found\n" );
+ $diffs = new Diff( explode( "\n", $before ), explode( "\n", $after ) );
+ $format = new UnifiedDiffFormatter();
+ return $format->format( $diffs );
+ }
+
+ # Make temporary files
+ $td = wfTempDir();
+ $oldtextFile = fopen( $oldtextName = tempnam( $td, 'merge-old-' ), 'w' );
+ $newtextFile = fopen( $newtextName = tempnam( $td, 'merge-your-' ), 'w' );
+
+ fwrite( $oldtextFile, $before ); fclose( $oldtextFile );
+ fwrite( $newtextFile, $after ); fclose( $newtextFile );
+
+ // Get the diff of the two files
+ $cmd = "$wgDiff " . $params . ' ' .wfEscapeShellArg( $oldtextName, $newtextName );
+
+ $h = popen( $cmd, 'r' );
+
+ $diff = '';
+
+ do {
+ $data = fread( $h, 8192 );
+ if ( strlen( $data ) == 0 ) {
+ break;
+ }
+ $diff .= $data;
+ } while ( true );
+
+ // Clean up
+ pclose( $h );
+ unlink( $oldtextName );
+ unlink( $newtextName );
+
+ // Kill the --- and +++ lines. They're not useful.
+ $diff_lines = explode( "\n", $diff );
+ if (strpos( $diff_lines[0], '---' ) === 0) {
+ unset($diff_lines[0]);
+ }
+ if (strpos( $diff_lines[1], '+++' ) === 0) {
+ unset($diff_lines[1]);
+ }
+
+ $diff = implode( "\n", $diff_lines );
+
+ return $diff;
+}
+
+/**
* @todo document
*/
function wfVarDump( $var ) {
@@ -1208,7 +1345,7 @@ function wfClearOutputBuffers() {
function wfAcceptToPrefs( $accept, $def = '*/*' ) {
# No arg means accept anything (per HTTP spec)
if( !$accept ) {
- return array( $def => 1 );
+ return array( $def => 1.0 );
}
$prefs = array();
@@ -1217,12 +1354,12 @@ function wfAcceptToPrefs( $accept, $def = '*/*' ) {
foreach( $parts as $part ) {
# FIXME: doesn't deal with params like 'text/html; level=1'
- @list( $value, $qpart ) = explode( ';', $part );
+ @list( $value, $qpart ) = explode( ';', trim( $part ) );
$match = array();
if( !isset( $qpart ) ) {
- $prefs[$value] = 1;
+ $prefs[$value] = 1.0;
} elseif( preg_match( '/q\s*=\s*(\d*\.\d+)/', $qpart, $match ) ) {
- $prefs[$value] = $match[1];
+ $prefs[$value] = floatval($match[1]);
}
}
@@ -1415,41 +1552,35 @@ function wfTimestamp($outputtype=TS_UNIX,$ts=0) {
$uts=time();
} elseif (preg_match('/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)$/D',$ts,$da)) {
# TS_DB
- $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6],
- (int)$da[2],(int)$da[3],(int)$da[1]);
} elseif (preg_match('/^(\d{4}):(\d\d):(\d\d) (\d\d):(\d\d):(\d\d)$/D',$ts,$da)) {
# TS_EXIF
- $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6],
- (int)$da[2],(int)$da[3],(int)$da[1]);
} elseif (preg_match('/^(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/D',$ts,$da)) {
# TS_MW
- $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6],
- (int)$da[2],(int)$da[3],(int)$da[1]);
- } elseif (preg_match('/^(\d{1,13})$/D',$ts,$da)) {
+ } elseif (preg_match('/^\d{1,13}$/D',$ts)) {
# TS_UNIX
$uts = $ts;
- } elseif (preg_match('/^(\d{1,2})-(...)-(\d\d(\d\d)?) (\d\d)\.(\d\d)\.(\d\d)/', $ts, $da)) {
+ } elseif (preg_match('/^\d{1,2}-...-\d\d(?:\d\d)? \d\d\.\d\d\.\d\d/', $ts)) {
# 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)) {
# TS_ISO_8601
- $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6],
- (int)$da[2],(int)$da[3],(int)$da[1]);
} elseif (preg_match('/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d)[\+\- ](\d\d)$/',$ts,$da)) {
# TS_POSTGRES
- $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6],
- (int)$da[2],(int)$da[3],(int)$da[1]);
} elseif (preg_match('/^(\d{4})\-(\d\d)\-(\d\d) (\d\d):(\d\d):(\d\d) GMT$/',$ts,$da)) {
# TS_POSTGRES
- $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6],
- (int)$da[2],(int)$da[3],(int)$da[1]);
} else {
# Bogus value; fall back to the epoch...
wfDebug("wfTimestamp() fed bogus time value: $outputtype; $ts\n");
$uts = 0;
}
+ if (count( $da ) ) {
+ // Warning! gmmktime() acts oddly if the month or day is set to 0
+ // We may want to handle that explicitly at some point
+ $uts=gmmktime((int)$da[4],(int)$da[5],(int)$da[6],
+ (int)$da[2],(int)$da[3],(int)$da[1]);
+ }
switch($outputtype) {
case TS_UNIX:
@@ -1515,9 +1646,9 @@ function wfGetCachedNotice( $name ) {
global $wgOut, $parserMemc;
$fname = 'wfGetCachedNotice';
wfProfileIn( $fname );
-
+
$needParse = false;
-
+
if( $name === 'default' ) {
// special case
global $wgSiteNotice;
@@ -1533,7 +1664,7 @@ function wfGetCachedNotice( $name ) {
return( false );
}
}
-
+
$cachedNotice = $parserMemc->get( wfMemcKey( $name ) );
if( is_array( $cachedNotice ) ) {
if( md5( $notice ) == $cachedNotice['hash'] ) {
@@ -1544,7 +1675,7 @@ function wfGetCachedNotice( $name ) {
} else {
$needParse = true;
}
-
+
if( $needParse ) {
if( is_object( $wgOut ) ) {
$parsed = $wgOut->parse( $notice );
@@ -1555,21 +1686,21 @@ function wfGetCachedNotice( $name ) {
$notice = '';
}
}
-
+
wfProfileOut( $fname );
return $notice;
}
function wfGetNamespaceNotice() {
global $wgTitle;
-
+
# Paranoia
if ( !isset( $wgTitle ) || !is_object( $wgTitle ) )
return "";
$fname = 'wfGetNamespaceNotice';
wfProfileIn( $fname );
-
+
$key = "namespacenotice-" . $wgTitle->getNsText();
$namespaceNotice = wfGetCachedNotice( $key );
if ( $namespaceNotice && substr ( $namespaceNotice , 0 ,7 ) != "<p>&lt;" ) {
@@ -1586,8 +1717,8 @@ function wfGetSiteNotice() {
global $wgUser, $wgSiteNotice;
$fname = 'wfGetSiteNotice';
wfProfileIn( $fname );
- $siteNotice = '';
-
+ $siteNotice = '';
+
if( wfRunHooks( 'SiteNoticeBefore', array( &$siteNotice ) ) ) {
if( is_object( $wgUser ) && $wgUser->isLoggedIn() ) {
$siteNotice = wfGetCachedNotice( 'sitenotice' );
@@ -1609,7 +1740,7 @@ function wfGetSiteNotice() {
return $siteNotice;
}
-/**
+/**
* BC wrapper for MimeMagic::singleton()
* @deprecated
*/
@@ -1640,13 +1771,70 @@ function wfTempDir() {
/**
* Make directory, and make all parent directories if they don't exist
+ *
+ * @param string $fullDir Full path to directory to create
+ * @param int $mode Chmod value to use, default is $wgDirectoryMode
+ * @return bool
*/
-function wfMkdirParents( $fullDir, $mode = 0777 ) {
+function wfMkdirParents( $fullDir, $mode = null ) {
+ global $wgDirectoryMode;
if( strval( $fullDir ) === '' )
return true;
if( file_exists( $fullDir ) )
return true;
- return mkdir( str_replace( '/', DIRECTORY_SEPARATOR, $fullDir ), $mode, 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
+ 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;
+ }
+
+ 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;
}
/**
@@ -1654,7 +1842,7 @@ function wfMkdirParents( $fullDir, $mode = 0777 ) {
*/
function wfIncrStats( $key ) {
global $wgStatsMethod;
-
+
if( $wgStatsMethod == 'udp' ) {
global $wgUDPProfilerHost, $wgUDPProfilerPort, $wgDBname;
static $socket;
@@ -1693,15 +1881,12 @@ function wfPercent( $nr, $acc = 2, $round = true ) {
* @param string $userid ID of the user
* @param string $password Password of the user
* @return string Hashed password
+ * @deprecated Use User::crypt() or User::oldCrypt() instead
*/
function wfEncryptPassword( $userid, $password ) {
- global $wgPasswordSalt;
- $p = md5( $password);
-
- if($wgPasswordSalt)
- return md5( "{$userid}-{$p}" );
- else
- return $p;
+ wfDeprecated(__FUNCTION__);
+ # Just wrap around User::oldCrypt()
+ return User::oldCrypt($password, $userid);
}
/**
@@ -1809,7 +1994,7 @@ function wfIniGetBool( $setting ) {
*/
function wfShellExec( $cmd, &$retval=null ) {
global $IP, $wgMaxShellMemory, $wgMaxShellFileSize;
-
+
if( wfIniGetBool( 'safe_mode' ) ) {
wfDebug( "wfShellExec can't run in safe_mode, PHP's exec functions are too broken.\n" );
$retval = 1;
@@ -1833,14 +2018,14 @@ function wfShellExec( $cmd, &$retval=null ) {
$cmd = '"' . $cmd . '"';
}
wfDebug( "wfShellExec: $cmd\n" );
-
+
$retval = 1; // error by default?
ob_start();
passthru( $cmd, $retval );
$output = ob_get_contents();
ob_end_clean();
return $output;
-
+
}
/**
@@ -1899,7 +2084,7 @@ function wfRegexReplacement( $string ) {
*
* PHP's basename() only considers '\' a pathchar on Windows and Netware.
* We'll consider it so always, as we don't want \s in our Unix paths either.
- *
+ *
* @param string $path
* @param string $suffix to remove if present
* @return string
@@ -1929,14 +2114,14 @@ function wfRelativePath( $path, $from ) {
// Normalize mixed input on Windows...
$path = str_replace( '/', DIRECTORY_SEPARATOR, $path );
$from = str_replace( '/', DIRECTORY_SEPARATOR, $from );
-
+
// Trim trailing slashes -- fix for drive root
$path = rtrim( $path, DIRECTORY_SEPARATOR );
$from = rtrim( $from, DIRECTORY_SEPARATOR );
-
+
$pieces = explode( DIRECTORY_SEPARATOR, dirname( $path ) );
$against = explode( DIRECTORY_SEPARATOR, $from );
-
+
if( $pieces[0] !== $against[0] ) {
// Non-matching Windows drive letters?
// Return a full path.
@@ -2002,7 +2187,7 @@ function wfMakeUrlIndex( $url ) {
$delimiter = ':';
// parse_url detects for news: and mailto: the host part of an url as path
// We have to correct this wrong detection
- if ( isset ( $bits['path'] ) ) {
+ if ( isset ( $bits['path'] ) ) {
$bits['host'] = $bits['path'];
$bits['path'] = '';
}
@@ -2099,7 +2284,7 @@ function wfBaseConvert( $input, $sourceBase, $destBase, $pad=1, $lowercase=true
$digitChars = ( $lowercase ) ? '0123456789abcdefghijklmnopqrstuvwxyz' : '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
$inDigits = array();
$outChars = '';
-
+
// Decode and validate input string
$input = strtolower( $input );
for( $i = 0; $i < strlen( $input ); $i++ ) {
@@ -2109,18 +2294,18 @@ function wfBaseConvert( $input, $sourceBase, $destBase, $pad=1, $lowercase=true
}
$inDigits[] = $n;
}
-
+
// Iterate over the input, modulo-ing out an output digit
// at a time until input is gone.
while( count( $inDigits ) ) {
$work = 0;
$workDigits = array();
-
+
// Long division...
foreach( $inDigits as $digit ) {
$work *= $sourceBase;
$work += $digit;
-
+
if( $work < $destBase ) {
// Gonna need to pull another digit.
if( count( $workDigits ) ) {
@@ -2132,26 +2317,26 @@ function wfBaseConvert( $input, $sourceBase, $destBase, $pad=1, $lowercase=true
} else {
// Finally! Actual division!
$workDigits[] = intval( $work / $destBase );
-
+
// Isn't it annoying that most programming languages
// don't have a single divide-and-remainder operator,
// even though the CPU implements it that way?
$work = $work % $destBase;
}
}
-
+
// All that division leaves us with a remainder,
// which is conveniently our next output digit.
$outChars .= $digitChars[$work];
-
+
// And we continue!
$inDigits = $workDigits;
}
-
+
while( strlen( $outChars ) < $pad ) {
$outChars .= '0';
}
-
+
return strrev( $outChars );
}
@@ -2183,20 +2368,44 @@ function wfCreateObject( $name, $p ){
}
/**
- * Aliases for modularized functions
+ * Alias for modularized function
+ * @deprecated Use Http::get() instead
*/
-function wfGetHTTP( $url, $timeout = 'default' ) {
- return Http::get( $url, $timeout );
+function wfGetHTTP( $url, $timeout = 'default' ) {
+ wfDeprecated(__FUNCTION__);
+ return Http::get( $url, $timeout );
}
-function wfIsLocalURL( $url ) {
- return Http::isLocalURL( $url );
+
+/**
+ * Alias for modularized function
+ * @deprecated Use Http::isLocalURL() instead
+ */
+function wfIsLocalURL( $url ) {
+ wfDeprecated(__FUNCTION__);
+ return Http::isLocalURL( $url );
+}
+
+function wfHttpOnlySafe() {
+ global $wgHttpOnlyBlacklist;
+ if( !version_compare("5.2", PHP_VERSION, "<") )
+ return false;
+
+ if( isset( $_SERVER['HTTP_USER_AGENT'] ) ) {
+ foreach( $wgHttpOnlyBlacklist as $regex ) {
+ if( preg_match( $regex, $_SERVER['HTTP_USER_AGENT'] ) ) {
+ return false;
+ }
+ }
+ }
+
+ return true;
}
/**
* Initialise php session
*/
function wfSetupSession() {
- global $wgSessionsInMemcached, $wgCookiePath, $wgCookieDomain, $wgCookieSecure;
+ global $wgSessionsInMemcached, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookieHttpOnly;
if( $wgSessionsInMemcached ) {
require_once( 'MemcachedSessions.php' );
} elseif( 'files' != ini_get( 'session.save_handler' ) ) {
@@ -2204,9 +2413,25 @@ function wfSetupSession() {
# application, it will end up failing. Try to recover.
ini_set ( 'session.save_handler', 'files' );
}
- session_set_cookie_params( 0, $wgCookiePath, $wgCookieDomain, $wgCookieSecure);
+ $httpOnlySafe = wfHttpOnlySafe();
+ wfDebugLog( 'cookie',
+ 'session_set_cookie_params: "' . implode( '", "',
+ array(
+ 0,
+ $wgCookiePath,
+ $wgCookieDomain,
+ $wgCookieSecure,
+ $httpOnlySafe && $wgCookieHttpOnly ) ) . '"' );
+ if( $httpOnlySafe && $wgCookieHttpOnly ) {
+ session_set_cookie_params( 0, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookieHttpOnly );
+ } else {
+ // PHP 5.1 throws warnings if you pass the HttpOnly parameter for 5.2.
+ session_set_cookie_params( 0, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
+ }
session_cache_limiter( 'private, must-revalidate' );
- @session_start();
+ wfSuppressWarnings();
+ session_start();
+ wfRestoreWarnings();
}
/**
@@ -2253,13 +2478,8 @@ function wfFormatStackFrame($frame) {
* Get a cache key
*/
function wfMemcKey( /*... */ ) {
- global $wgDBprefix, $wgDBname;
$args = func_get_args();
- if ( $wgDBprefix ) {
- $key = "$wgDBname-$wgDBprefix:" . implode( ':', $args );
- } else {
- $key = $wgDBname . ':' . implode( ':', $args );
- }
+ $key = wfWikiID() . ':' . implode( ':', $args );
return $key;
}
@@ -2280,42 +2500,80 @@ function wfForeignMemcKey( $db, $prefix /*, ... */ ) {
* Get an ASCII string identifying this wiki
* This is used as a prefix in memcached keys
*/
-function wfWikiID() {
- global $wgDBprefix, $wgDBname;
- if ( $wgDBprefix ) {
- return "$wgDBname-$wgDBprefix";
+function wfWikiID( $db = null ) {
+ if( $db instanceof Database ) {
+ return $db->getWikiID();
} else {
- return $wgDBname;
+ global $wgDBprefix, $wgDBname;
+ if ( $wgDBprefix ) {
+ return "$wgDBname-$wgDBprefix";
+ } else {
+ return $wgDBname;
+ }
}
}
+/**
+ * Split a wiki ID into DB name and table prefix
+ */
+function wfSplitWikiID( $wiki ) {
+ $bits = explode( '-', $wiki, 2 );
+ if ( count( $bits ) < 2 ) {
+ $bits[] = '';
+ }
+ return $bits;
+}
+
/*
- * Get a Database object
- * @param integer $db Index of the connection to get. May be DB_MASTER for the
- * master (for write queries), DB_SLAVE for potentially lagged
+ * Get a Database object.
+ * @param integer $db Index of the connection to get. May be DB_MASTER for the
+ * master (for write queries), DB_SLAVE for potentially lagged
* read queries, or an integer >= 0 for a particular server.
*
- * @param mixed $groups Query groups. An array of group names that this query
- * belongs to. May contain a single string if the query is only
+ * @param mixed $groups Query groups. An array of group names that this query
+ * belongs to. May contain a single string if the query is only
* in one group.
+ *
+ * @param string $wiki The wiki ID, or false for the current wiki
+ *
+ * Note: multiple calls to wfGetDB(DB_SLAVE) during the course of one request
+ * will always return the same object, unless the underlying connection or load
+ * balancer is manually destroyed.
*/
-function &wfGetDB( $db = DB_LAST, $groups = array() ) {
- global $wgLoadBalancer;
- $ret = $wgLoadBalancer->getConnection( $db, true, $groups );
- return $ret;
+function &wfGetDB( $db = DB_LAST, $groups = array(), $wiki = false ) {
+ return wfGetLB( $wiki )->getConnection( $db, $groups, $wiki );
}
/**
- * Find a file.
+ * Get a load balancer object.
+ *
+ * @param array $groups List of query groups
+ * @param string $wiki Wiki ID, or false for the current wiki
+ * @return LoadBalancer
+ */
+function wfGetLB( $wiki = false ) {
+ return wfGetLBFactory()->getMainLB( $wiki );
+}
+
+/**
+ * Get the load balancer factory object
+ */
+function &wfGetLBFactory() {
+ return LBFactory::singleton();
+}
+
+/**
+ * Find a file.
* Shortcut for RepoGroup::singleton()->findFile()
* @param mixed $title Title object or string. May be interwiki.
- * @param mixed $time Requested time for an archived image, or false for the
- * current version. An image object will be returned which
- * existed at the specified time.
+ * @param mixed $time Requested time for an archived image, or false for the
+ * current version. An image object will be returned which
+ * was created at the specified time.
+ * @param mixed $flags FileRepo::FIND_ flags
* @return File, or false if the file does not exist
*/
-function wfFindFile( $title, $time = false ) {
- return RepoGroup::singleton()->findFile( $title, $time );
+function wfFindFile( $title, $time = false, $flags = 0 ) {
+ return RepoGroup::singleton()->findFile( $title, $time, $flags );
}
/**
@@ -2364,13 +2622,39 @@ function wfBoolToStr( $value ) {
/**
* Load an extension messages file
+ *
+ * @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).
*/
-function wfLoadExtensionMessages( $extensionName ) {
- global $wgExtensionMessagesFiles, $wgMessageCache;
- if ( !empty( $wgExtensionMessagesFiles[$extensionName] ) ) {
- $wgMessageCache->loadMessagesFile( $wgExtensionMessagesFiles[$extensionName] );
- // Prevent double-loading
- $wgExtensionMessagesFiles[$extensionName] = false;
+function wfLoadExtensionMessages( $extensionName, $langcode = false ) {
+ global $wgExtensionMessagesFiles, $wgMessageCache, $wgLang, $wgContLang;
+
+ #For recording whether extension message files have been loaded in a given language.
+ static $loaded = array();
+
+ if( !array_key_exists( $extensionName, $loaded ) ) {
+ $loaded[$extensionName] = array();
+ }
+
+ if ( !isset($wgExtensionMessagesFiles[$extensionName]) ) {
+ throw new MWException( "Messages file for extensions $extensionName is not defined" );
+ }
+
+ if( !$langcode && !array_key_exists( '*', $loaded[$extensionName] ) ) {
+ # Just do en, content language and user language.
+ $wgMessageCache->loadMessagesFile( $wgExtensionMessagesFiles[$extensionName], false );
+ # Mark that they have been loaded.
+ $loaded[$extensionName]['en'] = true;
+ $loaded[$extensionName][$wgLang->getCode()] = true;
+ $loaded[$extensionName][$wgContLang->getCode()] = true;
+ # Mark that this part has been done to avoid weird if statements.
+ $loaded[$extensionName]['*'] = true;
+ } elseif( is_string( $langcode ) && !array_key_exists( $langcode, $loaded[$extensionName] ) ) {
+ # Load messages for specified language.
+ $wgMessageCache->loadMessagesFile( $wgExtensionMessagesFiles[$extensionName], $langcode );
+ # Mark that they have been loaded.
+ $loaded[$extensionName][$langcode] = true;
}
}
@@ -2388,7 +2672,7 @@ function wfGetNull() {
/**
* Displays a maxlag error
- *
+ *
* @param string $host Server that lags the most
* @param int $lag Maxlag (actual)
* @param int $maxLag Maxlag (requested)
@@ -2405,3 +2689,81 @@ function wfMaxlagError( $host, $lag, $maxLag ) {
echo "Waiting for a database server: $lag seconds lagged\n";
}
}
+
+/**
+ * Throws an E_USER_NOTICE saying that $function is deprecated
+ * @param string $function
+ * @return null
+ */
+function wfDeprecated( $function ) {
+ global $wgDebugLogFile;
+ if ( !$wgDebugLogFile ) {
+ return;
+ }
+ $callers = wfDebugBacktrace();
+ if( isset( $callers[2] ) ){
+ $callerfunc = $callers[2];
+ $callerfile = $callers[1];
+ if( isset( $callerfile['file'] ) && isset( $callerfile['line'] ) ){
+ $file = $callerfile['file'] . ' at line ' . $callerfile['line'];
+ } else {
+ $file = '(internal function)';
+ }
+ $func = '';
+ if( isset( $callerfunc['class'] ) )
+ $func .= $callerfunc['class'] . '::';
+ $func .= @$callerfunc['function'];
+ $msg = "Use of $function is deprecated. Called from $func in $file";
+ } else {
+ $msg = "Use of $function is deprecated.";
+ }
+ wfDebug( "$msg\n" );
+}
+
+/**
+ * Sleep until the worst slave's replication lag is less than or equal to
+ * $maxLag, in seconds. Use this when updating very large numbers of rows, as
+ * in maintenance scripts, to avoid causing too much lag. Of course, this is
+ * a no-op if there are no slaves.
+ *
+ * Every time the function has to wait for a slave, it will print a message to
+ * that effect (and then sleep for a little while), so it's probably not best
+ * to use this outside maintenance scripts in its present form.
+ *
+ * @param int $maxLag
+ * @return null
+ */
+function wfWaitForSlaves( $maxLag ) {
+ if( $maxLag ) {
+ $lb = wfGetLB();
+ list( $host, $lag ) = $lb->getMaxLag();
+ while( $lag > $maxLag ) {
+ $name = @gethostbyaddr( $host );
+ if( $name !== false ) {
+ $host = $name;
+ }
+ print "Waiting for $host (lagged $lag seconds)...\n";
+ sleep($maxLag);
+ list( $host, $lag ) = $lb->getMaxLag();
+ }
+ }
+}
+
+/** Generate a random 32-character hexadecimal token.
+ * @param mixed $salt Some sort of salt, if necessary, to add to random characters before hashing.
+ */
+function wfGenerateToken( $salt = '' ) {
+ $salt = serialize($salt);
+
+ return md5( mt_rand( 0, 0x7fffffff ) . $salt );
+}
+
+/**
+ * Replace all invalid characters with -
+ * @param mixed $title Filename to process
+ */
+function wfStripIllegalFilenameChars( $name ) {
+ $name = wfBaseName( $name );
+ $name = preg_replace ( "/[^".Title::legalChars()."]|:/", '-', $name );
+ return $name;
+}
diff --git a/includes/HTMLCacheUpdate.php b/includes/HTMLCacheUpdate.php
index 050005dd..1f250214 100644
--- a/includes/HTMLCacheUpdate.php
+++ b/includes/HTMLCacheUpdate.php
@@ -5,27 +5,28 @@
* Small numbers of links will be done immediately, large numbers are pushed onto
* the job queue.
*
- * This class is designed to work efficiently with small numbers of links, and
+ * This class is designed to work efficiently with small numbers of links, and
* to work reasonably well with up to ~10^5 links. Above ~10^6 links, the memory
* and time requirements of loading all backlinked IDs in doUpdate() might become
* prohibitive. The requirements measured at Wikimedia are approximately:
- *
+ *
* memory: 48 bytes per row
* time: 16us per row for the query plus processing
*
* The reason this query is done is to support partitioning of the job
- * by backlinked ID. The memory issue could be allieviated by doing this query in
+ * by backlinked ID. The memory issue could be allieviated by doing this query in
* batches, but of course LIMIT with an offset is inefficient on the DB side.
*
- * The class is nevertheless a vast improvement on the previous method of using
+ * The class is nevertheless a vast improvement on the previous method of using
* Image::getLinksTo() and Title::touchArray(), which uses about 2KB of memory per
* link.
+ *
+ * @ingroup Cache
*/
class HTMLCacheUpdate
{
public $mTitle, $mTable, $mPrefix;
public $mRowsPerJob, $mRowsPerQuery;
- public $mResult;
function __construct( $titleTo, $table ) {
global $wgUpdateRowsPerJob, $wgUpdateRowsPerQuery;
@@ -41,7 +42,7 @@ class HTMLCacheUpdate
$cond = $this->getToCondition();
$dbr = wfGetDB( DB_SLAVE );
$res = $dbr->select( $this->mTable, $this->getFromField(), $cond, __METHOD__ );
- $this->mResult = $res;
+
if ( $dbr->numRows( $res ) != 0 ) {
if ( $dbr->numRows( $res ) > $this->mRowsPerJob ) {
$this->insertJobs( $res );
@@ -67,7 +68,7 @@ class HTMLCacheUpdate
break;
}
}
-
+
$params = array(
'table' => $this->mTable,
'start' => $start,
@@ -88,7 +89,7 @@ class HTMLCacheUpdate
'categorylinks' => 'cl',
'templatelinks' => 'tl',
'redirect' => 'rd',
-
+
# Not needed
# 'externallinks' => 'el',
# 'langlinks' => 'll'
@@ -102,7 +103,7 @@ class HTMLCacheUpdate
}
return $this->mPrefix;
}
-
+
function getFromField() {
return $this->getPrefix() . '_from';
}
@@ -113,7 +114,7 @@ class HTMLCacheUpdate
case 'pagelinks':
case 'templatelinks':
case 'redirect':
- return array(
+ return array(
"{$prefix}_namespace" => $this->mTitle->getNamespace(),
"{$prefix}_title" => $this->mTitle->getDBkey()
);
@@ -138,7 +139,7 @@ class HTMLCacheUpdate
$dbw = wfGetDB( DB_MASTER );
$timestamp = $dbw->timestamp();
$done = false;
-
+
while ( !$done ) {
# Get all IDs in this query into an array
$ids = array();
@@ -155,10 +156,10 @@ class HTMLCacheUpdate
if ( !count( $ids ) ) {
break;
}
-
+
# Update page_touched
- $dbw->update( 'page',
- array( 'page_touched' => $timestamp ),
+ $dbw->update( 'page',
+ array( 'page_touched' => $timestamp ),
array( 'page_id IN (' . $dbw->makeList( $ids ) . ')' ),
__METHOD__
);
@@ -185,6 +186,7 @@ class HTMLCacheUpdate
/**
* @todo document (e.g. one-sentence top-level class description).
+ * @ingroup JobQueue
*/
class HTMLCacheUpdateJob extends Job {
var $table, $start, $end;
@@ -216,9 +218,8 @@ class HTMLCacheUpdateJob extends Job {
$dbr = wfGetDB( DB_SLAVE );
$res = $dbr->select( $this->table, $fromField, $conds, __METHOD__ );
- $update->invalidateIDs( new ResultWrapper( $dbr, $res ) );
+ $update->invalidateIDs( $res );
return true;
}
}
-
diff --git a/includes/HTMLFileCache.php b/includes/HTMLFileCache.php
index a7466814..ba2196eb 100644
--- a/includes/HTMLFileCache.php
+++ b/includes/HTMLFileCache.php
@@ -1,7 +1,8 @@
<?php
/**
* Contain the HTMLFileCache class
- * @addtogroup Cache
+ * @file
+ * @ingroup Cache
*/
/**
@@ -10,11 +11,13 @@
* emergency abort/fallback to cache.
*
* Global options that affect this module:
- * $wgCachePages
- * $wgCacheEpoch
- * $wgUseFileCache
- * $wgFileCacheDirectory
- * $wgUseGzip
+ * - $wgCachePages
+ * - $wgCacheEpoch
+ * - $wgUseFileCache
+ * - $wgFileCacheDirectory
+ * - $wgUseGzip
+ *
+ * @ingroup Cache
*/
class HTMLFileCache {
var $mTitle, $mFileCache;
@@ -153,5 +156,3 @@ class HTMLFileCache {
}
}
-
-
diff --git a/includes/HistoryBlob.php b/includes/HistoryBlob.php
index 984ee2d4..3772926d 100644
--- a/includes/HistoryBlob.php
+++ b/includes/HistoryBlob.php
@@ -1,11 +1,8 @@
<?php
-/**
- *
- */
/**
* Pure virtual parent
- * @todo document (needs a one-sentence top-level class description, that answers the question: "what is a HistoryBlob?")
+ * @todo document (needs a one-sentence top-level class description, that answers the question: "what is a HistoryBlob?")
*/
interface HistoryBlob
{
@@ -308,6 +305,3 @@ class HistoryBlobCurStub {
return $row->cur_text;
}
}
-
-
-
diff --git a/includes/Hooks.php b/includes/Hooks.php
index 20103db4..046a149d 100644
--- a/includes/Hooks.php
+++ b/includes/Hooks.php
@@ -19,6 +19,7 @@
*
* @author Evan Prodromou <evan@wikitravel.org>
* @see hooks.txt
+ * @file
*/
@@ -27,7 +28,7 @@
* careful about its contents. So, there's a lot more error-checking
* in here than would normally be necessary.
*/
-function wfRunHooks($event, $args = null) {
+function wfRunHooks($event, $args = array()) {
global $wgHooks;
@@ -108,6 +109,9 @@ function wfRunHooks($event, $args = null) {
$callback = $func;
}
+ // Run autoloader (workaround for call_user_func_array bug)
+ is_callable( $callback );
+
/* Call the hook. */
wfProfileIn( $func );
$retval = call_user_func_array( $callback, $hook_args );
@@ -140,4 +144,3 @@ function wfRunHooks($event, $args = null) {
return true;
}
-
diff --git a/includes/HttpFunctions.php b/includes/HttpFunctions.php
index 6ea3abd0..555a79b7 100644
--- a/includes/HttpFunctions.php
+++ b/includes/HttpFunctions.php
@@ -24,7 +24,7 @@ class Http {
# Use curl if available
if ( function_exists( 'curl_init' ) ) {
$c = curl_init( $url );
- if ( wfIsLocalURL( $url ) ) {
+ if ( self::isLocalURL( $url ) ) {
curl_setopt( $c, CURLOPT_PROXY, 'localhost:80' );
} else if ($wgHTTPProxy) {
curl_setopt($c, CURLOPT_PROXY, $wgHTTPProxy);
@@ -118,4 +118,3 @@ class Http {
return false;
}
}
-
diff --git a/includes/IP.php b/includes/IP.php
index db712c3b..e76f66c1 100644
--- a/includes/IP.php
+++ b/includes/IP.php
@@ -48,7 +48,7 @@ class IP {
// IPv6 IPs with two "::" strings are ambiguous and thus invalid
return preg_match( '/^' . IP_ADDRESS_STRING . '$/', $ip) && ( substr_count($ip, '::') < 2 );
}
-
+
public static function isIPv6( $ip ) {
if ( !$ip ) return false;
if( is_array( $ip ) ) {
@@ -57,18 +57,18 @@ class IP {
// IPv6 IPs with two "::" strings are ambiguous and thus invalid
return preg_match( '/^' . RE_IPV6_ADD . '(\/' . RE_IPV6_PREFIX . '|)$/', $ip) && ( substr_count($ip, '::') < 2);
}
-
+
public static function isIPv4( $ip ) {
if ( !$ip ) return false;
return preg_match( '/^' . RE_IP_ADD . '(\/' . RE_IP_PREFIX . '|)$/', $ip);
}
-
+
/**
* Given an IP address in dotted-quad notation, returns an IPv6 octet.
* See http://www.answers.com/topic/ipv4-compatible-address
* IPs with the first 92 bits as zeros are reserved from IPv6
* @param $ip quad-dotted IP address.
- * @return string
+ * @return string
*/
public static function IPv4toIPv6( $ip ) {
if ( !$ip ) return null;
@@ -106,13 +106,13 @@ class IP {
$r_ip = wfBaseConvert( $r_ip, 16, 10 );
return $r_ip;
}
-
+
/**
* Given an IPv6 address in octet notation, returns the expanded octet.
* IPv4 IPs will be trimmed, thats it...
* @param $ip octet ipv6 IP address.
- * @return string
- */
+ * @return string
+ */
public static function sanitizeIP( $ip ) {
$ip = trim( $ip );
if ( $ip === '' ) return null;
@@ -132,11 +132,11 @@ class IP {
$ip = preg_replace( '/(^|:)0+' . RE_IPV6_WORD . '/', '$1$2', $ip );
return $ip;
}
-
+
/**
* Given an unsigned integer, returns an IPv6 address in octet notation
* @param $ip integer IP address.
- * @return string
+ * @return string
*/
public static function toOctet( $ip_int ) {
// Convert to padded uppercase hex
@@ -181,9 +181,9 @@ class IP {
}
return array( $network, $bits );
}
-
+
/**
- * Given a string range in a number of formats, return the start and end of
+ * Given a string range in a number of formats, return the start and end of
* the range in hexadecimal. For IPv6.
*
* Formats are:
@@ -233,7 +233,7 @@ class IP {
return array( $start, $end );
}
}
-
+
/**
* Validate an IP address.
* @return boolean True if it is valid.
@@ -310,7 +310,7 @@ class IP {
* Return a zero-padded hexadecimal representation of an IP address.
*
* Hexadecimal addresses are used because they can easily be extended to
- * IPv6 support. To separate the ranges, the return value from this
+ * IPv6 support. To separate the ranges, the return value from this
* function for an IPv6 address will be prefixed with "v6-", a non-
* hexadecimal string which sorts after the IPv4 addresses.
*
@@ -396,14 +396,14 @@ class IP {
}
/**
- * Given a string range in a number of formats, return the start and end of
+ * Given a string range in a number of formats, return the start and end of
* the range in hexadecimal.
*
* Formats are:
* 1.2.3.4/24 CIDR
* 1.2.3.4 - 1.2.3.5 Explicit range
* 1.2.3.4 Single IP
- *
+ *
* 2001:0db8:85a3::7344/96 CIDR
* 2001:0db8:85a3::7344 - 2001:0db8:85a3::7344 Explicit range
* 2001:0db8:85a3::7344 Single IP
@@ -439,7 +439,7 @@ class IP {
}
if ( $start === false || $end === false ) {
return array( false, false );
- } else {
+ } else {
return array( $start, $end );
}
}
@@ -492,5 +492,3 @@ class IP {
return null; // give up
}
}
-
-
diff --git a/includes/ImageFunctions.php b/includes/ImageFunctions.php
index 3e87c994..af05c1c9 100644
--- a/includes/ImageFunctions.php
+++ b/includes/ImageFunctions.php
@@ -53,10 +53,10 @@ function wfGetSVGsize( $filename ) {
return false;
}
$tag = $matches[1];
- if( preg_match( '/\bwidth\s*=\s*("[^"]+"|\'[^\']+\')/s', $tag, $matches ) ) {
+ if( preg_match( '/(?:^|\s)width\s*=\s*("[^"]+"|\'[^\']+\')/s', $tag, $matches ) ) {
$width = wfScaleSVGUnit( trim( substr( $matches[1], 1, -1 ) ) );
}
- if( preg_match( '/\bheight\s*=\s*("[^"]+"|\'[^\']+\')/s', $tag, $matches ) ) {
+ if( preg_match( '/(?:^|\s)height\s*=\s*("[^"]+"|\'[^\']+\')/s', $tag, $matches ) ) {
$height = wfScaleSVGUnit( trim( substr( $matches[1], 1, -1 ) ) );
}
@@ -73,8 +73,8 @@ function wfGetSVGsize( $filename ) {
* * Any subsequent links on the same line are considered to be exceptions,
* i.e. articles where the image may occur inline.
*
- * @param string $name the image name to check
- * @param Title $contextTitle The page on which the image occurs, if known
+ * @param $name string the image name to check
+ * @param $contextTitle Title: the page on which the image occurs, if known
* @return bool
*/
function wfIsBadImage( $name, $contextTitle = false ) {
@@ -122,7 +122,7 @@ function wfIsBadImage( $name, $contextTitle = false ) {
}
}
}
-
+
$contextKey = $contextTitle ? $contextTitle->getPrefixedDBkey() : false;
$bad = isset( $badImages[$name] ) && !isset( $badImages[$name][$contextKey] );
wfProfileOut( __METHOD__ );
@@ -145,6 +145,3 @@ function wfFitBoxWidth( $boxWidth, $boxHeight, $maxHeight ) {
else
return $roundedUp;
}
-
-
-
diff --git a/includes/ImageGallery.php b/includes/ImageGallery.php
index 46ecd169..492a3e06 100644
--- a/includes/ImageGallery.php
+++ b/includes/ImageGallery.php
@@ -3,14 +3,11 @@ if ( ! defined( 'MEDIAWIKI' ) )
die( 1 );
/**
- */
-
-/**
* Image gallery
*
* Add images to the gallery using add(), then render that list to HTML using toHTML().
*
- * @addtogroup Media
+ * @ingroup Media
*/
class ImageGallery
{
@@ -37,7 +34,7 @@ class ImageGallery
private $mPerRow = 4; // How many images wide should the gallery be?
private $mWidths = 120, $mHeights = 120; // How wide/tall each thumbnail should be
-
+
private $mAttribs = array();
/**
@@ -196,11 +193,11 @@ class ImageGallery
function setShowFilename( $f ) {
$this->mShowFilename = ( $f == true);
}
-
+
/**
* Set arbitrary attributes to go on the HTML gallery output element.
* Should be suitable for a &lt;table&gt; element.
- *
+ *
* Note -- if taking from user input, you should probably run through
* Sanitizer::validateAttributes() first.
*
@@ -240,10 +237,10 @@ class ImageGallery
foreach ( $this->mImages as $pair ) {
$nt = $pair[0];
$text = $pair[1];
-
+
# Give extensions a chance to select the file revision for us
- $time = false;
- wfRunHooks( 'BeforeGalleryFindFile', array( &$this, &$nt, &$time ) );
+ $time = $descQuery = false;
+ wfRunHooks( 'BeforeGalleryFindFile', array( &$this, &$nt, &$time, &$descQuery ) );
$img = wfFindFile( $nt, $time );
@@ -261,14 +258,14 @@ class ImageGallery
. htmlspecialchars( $img->getLastError() ) . '</div>';
} else {
$vpad = floor( ( 1.25*$this->mHeights - $thumb->height ) /2 ) - 2;
-
+
$thumbhtml = "\n\t\t\t".
'<div class="thumb" style="padding: ' . $vpad . 'px 0; width: ' .($this->mWidths+30).'px;">'
# Auto-margin centering for block-level elements. Needed now that we have video
# handlers since they may emit block-level elements as opposed to simple <img> tags.
# ref http://css-discuss.incutio.com/?page=CenteringBlockElement
. '<div style="margin-left: auto; margin-right: auto; width: ' .$this->mWidths.'px;">'
- . $thumb->toHtml( array( 'desc-link' => true ) ) . '</div></div>';
+ . $thumb->toHtml( array( 'desc-link' => true, 'desc-query' => $descQuery ) ) . '</div></div>';
// Call parser transform hook
if ( $this->mParser && $img->getHandler() ) {
@@ -277,7 +274,7 @@ class ImageGallery
}
//TODO
- //$ul = $sk->makeLink( $wgContLang->getNsText( Namespace::getUser() ) . ":{$ut}", $ut );
+ //$ul = $sk->makeLink( $wgContLang->getNsText( MWNamespace::getUser() ) . ":{$ut}", $ut );
if( $this->mShowBytes ) {
if( $img ) {
@@ -328,7 +325,7 @@ class ImageGallery
public function count() {
return count( $this->mImages );
}
-
+
/**
* Set the contextual title
*
@@ -337,7 +334,7 @@ class ImageGallery
public function setContextTitle( $title ) {
$this->contextTitle = $title;
}
-
+
/**
* Get the contextual title, if applicable
*
@@ -350,5 +347,3 @@ class ImageGallery
}
} //class
-
-
diff --git a/includes/ImagePage.php b/includes/ImagePage.php
index 573bc4d7..30fcf13e 100644
--- a/includes/ImagePage.php
+++ b/includes/ImagePage.php
@@ -1,34 +1,46 @@
<?php
-/**
- */
-/**
- *
- */
if( !defined( 'MEDIAWIKI' ) )
die( 1 );
/**
* Special handling for image description pages
*
- * @addtogroup Media
+ * @ingroup Media
*/
class ImagePage extends Article {
- /* private */ var $img; // Image object this page is shown for
+ /* private */ var $img; // Image object
+ /* private */ var $displayImg;
/* private */ var $repo;
+ /* private */ var $fileLoaded;
var $mExtraDescription = false;
+ var $dupes;
- function __construct( $title, $time = false ) {
+ function __construct( $title ) {
parent::__construct( $title );
- $this->img = wfFindFile( $this->mTitle, $time );
+ $this->dupes = null;
+ $this->repo = null;
+ }
+
+ protected function loadFile() {
+ if ( $this->fileLoaded ) {
+ return true;
+ }
+ $this->fileLoaded = true;
+
+ $this->displayImg = $this->img = false;
+ wfRunHooks( 'ImagePageFindFile', array( $this, &$this->img, &$this->displayImg ) );
if ( !$this->img ) {
- $this->img = wfLocalFile( $this->mTitle );
- $this->current = $this->img;
- } else {
- $this->current = $time ? wfLocalFile( $this->mTitle ) : $this->img;
+ $this->img = wfFindFile( $this->mTitle );
+ if ( !$this->img ) {
+ $this->img = wfLocalFile( $this->mTitle );
+ }
+ }
+ if ( !$this->displayImg ) {
+ $this->displayImg = $this->img;
}
- $this->repo = $this->img->repo;
+ $this->repo = $this->img->getRepo();
}
/**
@@ -38,11 +50,28 @@ class ImagePage extends Article {
function render() {
global $wgOut;
$wgOut->setArticleBodyOnly( true );
- $wgOut->addSecondaryWikitext( $this->getContent() );
+ parent::view();
}
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() ) {
+ // mTitle is the same as the redirect target so ask Article
+ // to perform the redirect for us.
+ return Article::view();
+ } else {
+ // 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 );
+ $this->viewUpdates();
+ return;
+ }
+ }
$diff = $wgRequest->getVal( 'diff' );
$diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) );
@@ -50,16 +79,16 @@ class ImagePage extends Article {
if ( $this->mTitle->getNamespace() != NS_IMAGE || ( isset( $diff ) && $diffOnly ) )
return Article::view();
- if ($wgShowEXIF && $this->img->exists()) {
- // FIXME: bad interface, see note on MediaHandler::formatMetadata().
- $formattedMetadata = $this->img->formatMetadata();
+ if ( $wgShowEXIF && $this->displayImg->exists() ) {
+ // FIXME: bad interface, see note on MediaHandler::formatMetadata().
+ $formattedMetadata = $this->displayImg->formatMetadata();
$showmeta = $formattedMetadata !== false;
} else {
$showmeta = false;
}
- if ($this->img->exists())
- $wgOut->addHTML($this->showTOC($showmeta));
+ if ( $this->displayImg->exists() )
+ $wgOut->addHTML( $this->showTOC($showmeta) );
$this->openShowImage();
@@ -81,10 +110,23 @@ class ImagePage extends Article {
$wgOut->addWikiText( $fol );
}
$wgOut->addHTML( '<div id="shared-image-desc">' . $this->mExtraDescription . '</div>' );
+ } else {
+ $this->checkSharedConflict();
}
$this->closeShowImage();
$this->imageHistory();
+ // TODO: Cleanup the following
+
+ $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() ) {
+ $this->imageRedirects();
+ }
$this->imageLinks();
if ( $showmeta ) {
@@ -93,11 +135,83 @@ class ImagePage extends Article {
$collapse = htmlspecialchars( wfEscapeJsString( wfMsg( 'metadata-collapse' ) ) );
$wgOut->addHTML( Xml::element( 'h2', array( 'id' => 'metadata' ), wfMsg( 'metadata' ) ). "\n" );
$wgOut->addWikiText( $this->makeMetadataTable( $formattedMetadata ) );
+ $wgOut->addScriptFile( 'metadata.js' );
$wgOut->addHTML(
- "<script type=\"text/javascript\" src=\"$wgStylePath/common/metadata.js?$wgStyleVersion\"></script>\n" .
"<script type=\"text/javascript\">attachMetadataToggle('mw_metadata', '$expand', '$collapse');</script>\n" );
}
}
+
+ public function getRedirectTarget() {
+ $this->loadFile();
+ if ( $this->img->isLocal() ) {
+ return parent::getRedirectTarget();
+ }
+ // Foreign image page
+ $from = $this->img->getRedirected();
+ $to = $this->img->getName();
+ if ( $from == $to ) {
+ return null;
+ }
+ return $this->mRedirectTarget = Title::makeTitle( NS_IMAGE, $to );
+ }
+ public function followRedirect() {
+ $this->loadFile();
+ if ( $this->img->isLocal() ) {
+ return parent::followRedirect();
+ }
+ $from = $this->img->getRedirected();
+ $to = $this->img->getName();
+ if ( $from == $to ) {
+ return false;
+ }
+ return Title::makeTitle( NS_IMAGE, $to );
+ }
+ public function isRedirect( $text = false ) {
+ $this->loadFile();
+ if ( $this->img->isLocal() )
+ return parent::isRedirect( $text );
+
+ return (bool)$this->img->getRedirected();
+ }
+
+ public function isLocal() {
+ $this->loadFile();
+ return $this->img->isLocal();
+ }
+
+ public function getFile() {
+ $this->loadFile();
+ return $this->img;
+ }
+
+ public function getDisplayedFile() {
+ $this->loadFile();
+ return $this->displayImg;
+ }
+
+ public function getDuplicates() {
+ $this->loadFile();
+ if ( !is_null($this->dupes) ) {
+ return $this->dupes;
+ }
+ if ( !( $hash = $this->img->getSha1() ) ) {
+ return $this->dupes = array();
+ }
+ $dupes = RepoGroup::singleton()->findBySha1( $hash );
+ // Remove duplicates with self and non matching file sizes
+ $self = $this->img->getRepoName().':'.$this->img->getName();
+ $size = $this->img->getSize();
+ foreach ( $dupes as $index => $file ) {
+ $key = $file->getRepoName().':'.$file->getName();
+ if ( $key == $self )
+ unset( $dupes[$index] );
+ if ( $file->getSize() != $size )
+ unset( $dupes[$index] );
+ }
+ return $this->dupes = $dupes;
+
+ }
+
/**
* Create the TOC
@@ -121,7 +235,7 @@ class ImagePage extends Article {
/**
* Make a table with metadata to be shown in the output page.
*
- * FIXME: bad interface, see note on MediaHandler::formatMetadata().
+ * FIXME: bad interface, see note on MediaHandler::formatMetadata().
*
* @access private
*
@@ -148,11 +262,12 @@ class ImagePage extends Article {
/**
* Overloading Article's getContent method.
- *
+ *
* Omit noarticletext if sharedupload; text will be fetched from the
* shared upload server if possible.
*/
function getContent() {
+ $this->loadFile();
if( $this->img && !$this->img->isLocal() && 0 == $this->getID() ) {
return '';
}
@@ -162,7 +277,9 @@ class ImagePage extends Article {
function openShowImage() {
global $wgOut, $wgUser, $wgImageLimits, $wgRequest, $wgLang, $wgContLang;
- $full_url = $this->img->getURL();
+ $this->loadFile();
+
+ $full_url = $this->displayImg->getURL();
$linkAttribs = false;
$sizeSel = intval( $wgUser->getOption( 'imagesize') );
if( !isset( $wgImageLimits[$sizeSel] ) ) {
@@ -181,7 +298,7 @@ class ImagePage extends Article {
$sk = $wgUser->getSkin();
$dirmark = $wgContLang->getDirMark();
- if ( $this->img->exists() ) {
+ if ( $this->displayImg->exists() ) {
# image
$page = $wgRequest->getIntOrNull( 'page' );
if ( is_null( $page ) ) {
@@ -190,22 +307,22 @@ class ImagePage extends Article {
} else {
$params = array( 'page' => $page );
}
- $width_orig = $this->img->getWidth();
+ $width_orig = $this->displayImg->getWidth();
$width = $width_orig;
- $height_orig = $this->img->getHeight();
+ $height_orig = $this->displayImg->getHeight();
$height = $height_orig;
- $mime = $this->img->getMimeType();
+ $mime = $this->displayImg->getMimeType();
$showLink = false;
$linkAttribs = array( 'href' => $full_url );
- $longDesc = $this->img->getLongDesc();
+ $longDesc = $this->displayImg->getLongDesc();
wfRunHooks( 'ImageOpenShowImageInlineBefore', array( &$this , &$wgOut ) ) ;
- if ( $this->img->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->img->getSize() ), $mime );
+ #$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 ) {
# Calculate the thumbnail size.
@@ -226,43 +343,43 @@ class ImagePage extends Article {
array( 'parseinline' ), $wgLang->formatNum( $width ), $wgLang->formatNum( $height ) );
} else {
# Image is small enough to show full size on image page
- $msgbig = htmlspecialchars( $this->img->getName() );
+ $msgbig = htmlspecialchars( $this->displayImg->getName() );
$msgsmall = wfMsgExt( 'file-nohires', array( 'parseinline' ) );
}
$params['width'] = $width;
- $thumbnail = $this->img->transform( $params );
+ $thumbnail = $this->displayImg->transform( $params );
$anchorclose = "<br />";
- if( $this->img->mustRender() ) {
+ if( $this->displayImg->mustRender() ) {
$showLink = true;
} else {
- $anchorclose .=
+ $anchorclose .=
$msgsmall .
'<br />' . Xml::tags( 'a', $linkAttribs, $msgbig ) . "$dirmark " . $longDesc;
}
- if ( $this->img->isMultipage() ) {
+ if ( $this->displayImg->isMultipage() ) {
$wgOut->addHTML( '<table class="multipageimage"><tr><td>' );
}
if ( $thumbnail ) {
- $options = array(
- 'alt' => $this->img->getTitle()->getPrefixedText(),
+ $options = array(
+ 'alt' => $this->displayImg->getTitle()->getPrefixedText(),
'file-link' => true,
);
- $wgOut->addHTML( '<div class="fullImageLink" id="file">' .
+ $wgOut->addHTML( '<div class="fullImageLink" id="file">' .
$thumbnail->toHtml( $options ) .
$anchorclose . '</div>' );
}
- if ( $this->img->isMultipage() ) {
- $count = $this->img->pageCount();
+ if ( $this->displayImg->isMultipage() ) {
+ $count = $this->displayImg->pageCount();
if ( $page > 1 ) {
$label = $wgOut->parse( wfMsg( 'imgmultipageprev' ), false );
$link = $sk->makeKnownLinkObj( $this->mTitle, $label, 'page='. ($page-1) );
- $thumb1 = $sk->makeThumbLinkObj( $this->mTitle, $this->img, $link, $label, 'none',
+ $thumb1 = $sk->makeThumbLinkObj( $this->mTitle, $this->displayImg, $link, $label, 'none',
array( 'page' => $page - 1 ) );
} else {
$thumb1 = '';
@@ -271,34 +388,42 @@ class ImagePage extends Article {
if ( $page < $count ) {
$label = wfMsg( 'imgmultipagenext' );
$link = $sk->makeKnownLinkObj( $this->mTitle, $label, 'page='. ($page+1) );
- $thumb2 = $sk->makeThumbLinkObj( $this->mTitle, $this->img, $link, $label, 'none',
+ $thumb2 = $sk->makeThumbLinkObj( $this->mTitle, $this->displayImg, $link, $label, 'none',
array( 'page' => $page + 1 ) );
} else {
$thumb2 = '';
}
global $wgScript;
- $select = '<form name="pageselector" action="' .
- htmlspecialchars( $wgScript ) .
- '" method="get" onchange="document.pageselector.submit();">' .
- Xml::hidden( 'title', $this->getTitle()->getPrefixedDbKey() );
- $select .= $wgOut->parse( wfMsg( 'imgmultigotopre' ), false ) .
- ' <select id="pageselector" name="page">';
+
+ $formParams = array(
+ 'name' => 'pageselector',
+ 'action' => $wgScript,
+ 'onchange' => 'document.pageselector.submit();',
+ );
+
+ $option = array();
for ( $i=1; $i <= $count; $i++ ) {
- $select .= Xml::option( $wgLang->formatNum( $i ), $i,
- $i == $page );
+ $options[] = Xml::option( $wgLang->formatNum($i), $i, $i == $page );
}
- $select .= '</select>' . $wgOut->parse( wfMsg( 'imgmultigotopost' ), false ) .
- '<input type="submit" value="' .
- htmlspecialchars( wfMsg( 'imgmultigo' ) ) . '"></form>';
-
- $wgOut->addHTML( '</td><td><div class="multipageimagenavbox">' .
- "$select<hr />$thumb1\n$thumb2<br clear=\"all\" /></div></td></tr></table>" );
+ $select = Xml::tags( 'select',
+ array( 'id' => 'pageselector', 'name' => 'page' ),
+ implode( "\n", $options ) );
+
+ $wgOut->addHTML(
+ '</td><td><div class="multipageimagenavbox">' .
+ Xml::openElement( 'form', $formParams ) .
+ Xml::hidden( 'title', $this->getTitle()->getPrefixedDbKey() ) .
+ wfMsgExt( 'imgmultigoto', array( 'parseinline', 'replaceafter' ), $select ) .
+ Xml::submitButton( wfMsg( 'imgmultigo' ) ) .
+ Xml::closeElement( 'form' ) .
+ "<hr />$thumb1\n$thumb2<br clear=\"all\" /></div></td></tr></table>"
+ );
}
} else {
#if direct link is allowed but it's not a renderable image, show an icon.
- if ($this->img->isSafeFile()) {
- $icon= $this->img->iconThumb();
+ if ( $this->displayImg->isSafeFile() ) {
+ $icon= $this->displayImg->iconThumb();
$wgOut->addHTML( '<div class="fullImageLink" id="file">' .
$icon->toHtml( array( 'desc-link' => true ) ) .
@@ -310,9 +435,9 @@ class ImagePage extends Article {
if ($showLink) {
- $filename = wfEscapeWikiText( $this->img->getName() );
+ $filename = wfEscapeWikiText( $this->displayImg->getName() );
- if (!$this->img->isSafeFile()) {
+ if ( !$this->displayImg->isSafeFile() ) {
$warning = wfMsgNoTrans( 'mediawarning' );
$wgOut->addWikiText( <<<EOT
<div class="fullMedia">
@@ -333,7 +458,7 @@ EOT
}
}
- if(!$this->img->isLocal()) {
+ if( !$this->displayImg->isLocal() ) {
$this->printSharedImageText();
}
} else {
@@ -341,31 +466,90 @@ EOT
$title = SpecialPage::getTitleFor( 'Upload' );
$link = $sk->makeKnownLinkObj($title, wfMsgHtml('noimage-linktext'),
- 'wpDestFile=' . urlencode( $this->img->getName() ) );
+ 'wpDestFile=' . urlencode( $this->displayImg->getName() ) );
$wgOut->addHTML( wfMsgWikiHtml( 'noimage', $link ) );
}
}
+ /**
+ * Show a notice that the file is from a shared repository
+ */
function printSharedImageText() {
global $wgOut, $wgUser;
+ $this->loadFile();
+
$descUrl = $this->img->getDescriptionUrl();
$descText = $this->img->getDescriptionText();
- $s = "<div class='sharedUploadNotice'>" . wfMsgWikiHtml("sharedupload");
- if ( $descUrl && !$descText) {
+ $s = "<div class='sharedUploadNotice'>" . wfMsgWikiHtml( 'sharedupload' );
+ if ( $descUrl ) {
$sk = $wgUser->getSkin();
- $link = $sk->makeExternalLink( $descUrl, wfMsg('shareduploadwiki-linktext') );
- $s .= " " . wfMsgWikiHtml('shareduploadwiki', $link);
+ $link = $sk->makeExternalLink( $descUrl, wfMsg( 'shareduploadwiki-linktext' ) );
+ $msg = ( $descText ) ? 'shareduploadwiki-desc' : 'shareduploadwiki';
+ $msg = wfMsgExt( $msg, array( 'parseinline', 'replaceafter' ), $link );
+ if ( $msg != '-' ) {
+ # Show message only if not voided by local sysops
+ $s .= $msg;
+ }
}
$s .= "</div>";
- $wgOut->addHTML($s);
+ $wgOut->addHTML( $s );
if ( $descText ) {
$this->mExtraDescription = $descText;
}
}
+ /*
+ * Check for files with the same name on the foreign repos.
+ */
+ function checkSharedConflict() {
+ global $wgOut, $wgUser;
+
+ $repoGroup = RepoGroup::singleton();
+ if( !$repoGroup->hasForeignRepos() ) {
+ return;
+ }
+
+ $this->loadFile();
+ if( !$this->img->isLocal() ) {
+ return;
+ }
+
+ $this->dupFile = null;
+ $repoGroup->forEachForeignRepo( array( $this, 'checkSharedConflictCallback' ) );
+
+ if( !$this->dupFile )
+ return;
+ $dupfile = $this->dupFile;
+ $same = (
+ ($this->img->getSha1() == $dupfile->getSha1()) &&
+ ($this->img->getSize() == $dupfile->getSize())
+ );
+
+ $sk = $wgUser->getSkin();
+ $descUrl = $dupfile->getDescriptionUrl();
+ if( $same ) {
+ $link = $sk->makeExternalLink( $descUrl, wfMsg( 'shareduploadduplicate-linktext' ) );
+ $wgOut->addHTML( '<div id="shared-image-dup">' . wfMsgWikiHtml( 'shareduploadduplicate', $link ) . '</div>' );
+ } else {
+ $link = $sk->makeExternalLink( $descUrl, wfMsg( 'shareduploadconflict-linktext' ) );
+ $wgOut->addHTML( '<div id="shared-image-conflict">' . wfMsgWikiHtml( 'shareduploadconflict', $link ) . '</div>' );
+ }
+ }
+
+ function checkSharedConflictCallback( $repo ) {
+ $this->loadFile();
+ $dupfile = $repo->newFile( $this->img->getTitle() );
+ if( $dupfile && $dupfile->exists() ) {
+ $this->dupFile = $dupfile;
+ return $dupfile->exists();
+ }
+ return false;
+ }
+
function getUploadUrl() {
+ $this->loadFile();
$uploadTitle = SpecialPage::getTitleFor( 'Upload' );
return $uploadTitle->getFullUrl( 'wpDestFile=' . urlencode( $this->img->getName() ) );
}
@@ -377,23 +561,28 @@ EOT
function uploadLinksBox() {
global $wgUser, $wgOut;
+ $this->loadFile();
if( !$this->img->isLocal() )
return;
$sk = $wgUser->getSkin();
-
+
$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>" );
}
-
+
+ # Link to Special:FileDuplicateSearch
+ $dupeLink = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'FileDuplicateSearch', $this->mTitle->getDBkey() ), wfMsgHtml( 'imagepage-searchdupe' ) );
+ $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( '</ul>' );
}
@@ -409,29 +598,20 @@ EOT
*/
function imageHistory()
{
- global $wgUser, $wgOut, $wgUseExternalEditor;
-
- $sk = $wgUser->getSkin();
+ global $wgOut, $wgUseExternalEditor;
+ $this->loadFile();
if ( $this->img->exists() ) {
- $list = new ImageHistoryList( $sk, $this->current );
- $file = $this->current;
+ $list = new ImageHistoryList( $this );
+ $file = $this->img;
$dims = $file->getDimensionsString();
- $s = $list->beginImageHistoryList() .
- $list->imageHistoryLine( true, wfTimestamp(TS_MW, $file->getTimestamp()),
- $this->mTitle->getDBkey(), $file->getUser('id'),
- $file->getUser('text'), $file->getSize(), $file->getDescription(),
- $dims
- );
-
+ $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, wfTimestamp(TS_MW, $file->getTimestamp()),
- $file->getArchiveName(), $file->getUser('id'),
- $file->getUser('text'), $file->getSize(), $file->getDescription(),
- $dims
- );
+ $s .= $list->imageHistoryLine( false, $file );
}
$s .= $list->endImageHistoryList();
} else { $s=''; }
@@ -451,37 +631,97 @@ EOT
{
global $wgUser, $wgOut;
- $wgOut->addHTML( Xml::element( 'h2', array( 'id' => 'filelinks' ), wfMsg( 'imagelinks' ) ) . "\n" );
+ $limit = 100;
$dbr = wfGetDB( DB_SLAVE );
- $page = $dbr->tableName( 'page' );
- $imagelinks = $dbr->tableName( 'imagelinks' );
- $sql = "SELECT page_namespace,page_title FROM $imagelinks,$page WHERE il_to=" .
- $dbr->addQuotes( $this->mTitle->getDBkey() ) . " AND il_from=page_id";
- $sql = $dbr->limitResult($sql, 500, 0);
- $res = $dbr->query( $sql, "ImagePage::imageLinks" );
-
- if ( 0 == $dbr->numRows( $res ) ) {
- $wgOut->addHtml( '<p>' . wfMsg( "nolinkstoimage" ) . "</p>\n" );
+ $res = $dbr->select(
+ array( 'imagelinks', 'page' ),
+ array( 'page_namespace', 'page_title' ),
+ array( 'il_to' => $this->mTitle->getDBkey(), 'il_from = page_id' ),
+ __METHOD__,
+ array( 'LIMIT' => $limit + 1)
+ );
+ $count = $dbr->numRows( $res );
+ if ( $count == 0 ) {
+ $wgOut->addHTML( "<div id='mw-imagepage-nolinkstoimage'>\n" );
+ $wgOut->addWikiMsg( 'nolinkstoimage' );
+ $wgOut->addHTML( "</div>\n" );
return;
}
- $wgOut->addHTML( '<p>' . wfMsg( 'linkstoimage' ) . "</p>\n<ul>" );
+ $wgOut->addHTML( "<div id='mw-imagepage-section-linkstoimage'>\n" );
+ $wgOut->addWikiMsg( 'linkstoimage', $count );
+ $wgOut->addHTML( "<ul class='mw-imagepage-linktoimage'>\n" );
+
+ $sk = $wgUser->getSkin();
+ $count = 0;
+ while ( $s = $res->fetchObject() ) {
+ $count++;
+ 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, "" );
+ $wgOut->addHTML( "<li>{$link}</li>\n" );
+ }
+ }
+ $wgOut->addHTML( "</ul></div>\n" );
+ $res->free();
+
+ // Add a links to [[Special:Whatlinkshere]]
+ if ( $count > $limit )
+ $wgOut->addWikiMsg( 'morelinkstoimage', $this->mTitle->getPrefixedDBkey() );
+ }
+
+ function imageRedirects()
+ {
+ global $wgUser, $wgOut;
+
+ $redirects = $this->getTitle()->getRedirectsHere( NS_IMAGE );
+ if ( count( $redirects ) == 0 ) return;
+
+ $wgOut->addHTML( "<div id='mw-imagepage-section-redirectstofile'>\n" );
+ $wgOut->addWikiMsg( 'redirectstofile', count( $redirects ) );
+ $wgOut->addHTML( "<ul class='mw-imagepage-redirectstofile'>\n" );
+
+ $sk = $wgUser->getSkin();
+ foreach ( $redirects as $title ) {
+ $link = $sk->makeKnownLinkObj( $title, "", "redirect=no" );
+ $wgOut->addHTML( "<li>{$link}</li>\n" );
+ }
+ $wgOut->addHTML( "</ul></div>\n" );
+
+ }
+
+ function imageDupes() {
+ global $wgOut, $wgUser;
+
+ $this->loadFile();
+
+ $dupes = $this->getDuplicates();
+ if ( count( $dupes ) == 0 ) return;
+
+ $wgOut->addHTML( "<div id='mw-imagepage-section-duplicates'>\n" );
+ $wgOut->addWikiMsg( 'duplicatesoffile', count( $dupes ) );
+ $wgOut->addHTML( "<ul class='mw-imagepage-duplicates'>\n" );
$sk = $wgUser->getSkin();
- while ( $s = $dbr->fetchObject( $res ) ) {
- $name = Title::MakeTitle( $s->page_namespace, $s->page_title );
- $link = $sk->makeKnownLinkObj( $name, "" );
+ foreach ( $dupes as $file ) {
+ if ( $file->isLocal() )
+ $link = $sk->makeKnownLinkObj( $file->getTitle(), "" );
+ else
+ $link = $sk->makeExternalLink( $file->getDescriptionUrl(),
+ $file->getTitle()->getPrefixedText() );
$wgOut->addHTML( "<li>{$link}</li>\n" );
}
- $wgOut->addHTML( "</ul>\n" );
+ $wgOut->addHTML( "</ul></div>\n" );
}
/**
* Delete the file, or an earlier version of it
*/
public function delete() {
- if( !$this->img->exists() || !$this->img->isLocal() ) {
+ $this->loadFile();
+ if( !$this->img->exists() || !$this->img->isLocal() || $this->img->getRedirected() ) {
// Standard article deletion
Article::delete();
return;
@@ -494,14 +734,16 @@ EOT
* Revert the file to an earlier version
*/
public function revert() {
+ $this->loadFile();
$reverter = new FileRevertForm( $this->img );
$reverter->execute();
}
-
+
/**
* Override handling of action=purge
*/
function doPurge() {
+ $this->loadFile();
if( $this->img->exists() ) {
wfDebug( "ImagePage::doPurge purging " . $this->img->getName() . "\n" );
$update = new HTMLCacheUpdate( $this->mTitle, 'imagelinks' );
@@ -531,16 +773,31 @@ EOT
/**
* Builds the image revision log shown on image pages
*
- * @addtogroup Media
+ * @ingroup Media
*/
class ImageHistoryList {
- protected $img, $skin, $title, $repo;
+ protected $imagePage, $img, $skin, $title, $repo;
+
+ public function __construct( $imagePage ) {
+ global $wgUser;
+ $this->skin = $wgUser->getSkin();
+ $this->current = $imagePage->getFile();
+ $this->img = $imagePage->getDisplayedFile();
+ $this->title = $imagePage->getTitle();
+ $this->imagePage = $imagePage;
+ }
+
+ function getImagePage() {
+ return $this->imagePage;
+ }
- public function __construct( $skin, $img ) {
- $this->skin = $skin;
- $this->img = $img;
- $this->title = $img->getTitle();
+ function getSkin() {
+ return $this->skin;
+ }
+
+ function getFile() {
+ return $this->img;
}
public function beginImageHistoryList() {
@@ -549,12 +806,11 @@ class ImageHistoryList {
. $wgOut->parse( wfMsgNoTrans( 'filehist-help' ) )
. Xml::openElement( 'table', array( 'class' => 'filehistory' ) ) . "\n"
. '<tr><td></td>'
- . ( $this->img->isLocal() && $wgUser->isAllowed( 'delete' ) ? '<td></td>' : '' )
+ . ( $this->current->isLocal() && ($wgUser->isAllowed('delete') || $wgUser->isAllowed('deleterevision') ) ? '<td></td>' : '' )
. '<th>' . wfMsgHtml( 'filehist-datetime' ) . '</th>'
- . '<th>' . wfMsgHtml( 'filehist-user' ) . '</th>'
. '<th>' . wfMsgHtml( 'filehist-dimensions' ) . '</th>'
- . '<th class="mw-imagepage-filesize">' . wfMsgHtml( 'filehist-filesize' ) . '</th>'
- . '<th>' . wfMsgHtml( 'filehist-comment' ) . '</th>'
+ . '<th>' . wfMsgHtml( 'filehist-user' ) . '</th>'
+ . '<th>' . wfMsgHtml( 'filehist-comment' ) . '</th>'
. "</tr>\n";
}
@@ -562,36 +818,57 @@ class ImageHistoryList {
return "</table>\n";
}
- /**
- * Create one row of file history
- *
- * @param bool $iscur is this the current file version?
- * @param string $timestamp timestamp of file version
- * @param string $img filename
- * @param int $user ID of uploading user
- * @param string $usertext username of uploading user
- * @param int $size size of file version
- * @param string $description description of file version
- * @param string $dims dimensions of file version
- * @return string a HTML formatted table row
- */
- public function imageHistoryLine( $iscur, $timestamp, $img, $user, $usertext, $size, $description, $dims ) {
- global $wgUser, $wgLang, $wgContLang;
- $local = $this->img->isLocal();
- $row = '';
+ public function imageHistoryLine( $iscur, $file ) {
+ global $wgUser, $wgLang, $wgContLang, $wgTitle;
+
+ $timestamp = wfTimestamp(TS_MW, $file->getTimestamp());
+ $img = $iscur ? $file->getName() : $file->getArchiveName();
+ $user = $file->getUser('id');
+ $usertext = $file->getUser('text');
+ $size = $file->getSize();
+ $description = $file->getDescription();
+ $dims = $file->getDimensionsString();
+ $sha1 = $file->getSha1();
+
+ $local = $this->current->isLocal();
+ $row = $css = $selected = '';
// Deletion link
- if( $local && $wgUser->isAllowed( 'delete' ) ) {
+ if( $local && ($wgUser->isAllowed('delete') || $wgUser->isAllowed('deleterevision') ) ) {
$row .= '<td>';
- $q = array();
- $q[] = 'action=delete';
- if( !$iscur )
- $q[] = 'oldimage=' . urlencode( $img );
- $row .= $this->skin->makeKnownLinkObj(
- $this->title,
- wfMsgHtml( $iscur ? 'filehist-deleteall' : 'filehist-deleteone' ),
- implode( '&', $q )
- );
+ # Link to remove from history
+ if( $wgUser->isAllowed( 'delete' ) ) {
+ $q = array();
+ $q[] = 'action=delete';
+ if( !$iscur )
+ $q[] = 'oldimage=' . urlencode( $img );
+ $row .= $this->skin->makeKnownLinkObj(
+ $this->title,
+ wfMsgHtml( $iscur ? 'filehist-deleteall' : 'filehist-deleteone' ),
+ implode( '&', $q )
+ );
+ }
+ # Link to hide content
+ if( $wgUser->isAllowed( 'deleterevision' ) ) {
+ if( $wgUser->isAllowed('delete') ) {
+ $row .= '<br/>';
+ }
+ $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
+ // If file is top revision or locked from this user, don't link
+ if( $iscur || !$file->userCan(File::DELETED_RESTRICTED) ) {
+ $del = wfMsgHtml( 'rev-delundel' );
+ } else {
+ // If the file was hidden, link to sha-1
+ list($ts,$name) = explode('!',$img,2);
+ $del = $this->skin->makeKnownLinkObj( $revdel, wfMsg( 'rev-delundel' ),
+ 'target=' . urlencode( $wgTitle->getPrefixedText() ) .
+ '&oldimage=' . urlencode( $ts ) );
+ // Bolden oversighted content
+ if( $file->isDeleted(File::DELETED_RESTRICTED) )
+ $del = "<strong>$del</strong>";
+ }
+ $row .= "<tt style='white-space: nowrap;'><small>$del</small></tt>";
+ }
$row .= '</td>';
}
@@ -600,47 +877,73 @@ class ImageHistoryList {
if( $iscur ) {
$row .= wfMsgHtml( 'filehist-current' );
} elseif( $local && $wgUser->isLoggedIn() && $this->title->userCan( 'edit' ) ) {
- $q = array();
- $q[] = 'action=revert';
- $q[] = 'oldimage=' . urlencode( $img );
- $q[] = 'wpEditToken=' . urlencode( $wgUser->editToken( $img ) );
- $row .= $this->skin->makeKnownLinkObj(
- $this->title,
- wfMsgHtml( 'filehist-revert' ),
- implode( '&', $q )
- );
+ if( $file->isDeleted(File::DELETED_FILE) ) {
+ $row .= wfMsgHtml('filehist-revert');
+ } else {
+ $q = array();
+ $q[] = 'action=revert';
+ $q[] = 'oldimage=' . urlencode( $img );
+ $q[] = 'wpEditToken=' . urlencode( $wgUser->editToken( $img ) );
+ $row .= $this->skin->makeKnownLinkObj( $this->title,
+ wfMsgHtml( 'filehist-revert' ),
+ implode( '&', $q ) );
+ }
}
$row .= '</td>';
// Date/time and image link
- $row .= '<td>';
- $url = $iscur ? $this->img->getUrl() : $this->img->getArchiveUrl( $img );
- $row .= Xml::element(
- 'a',
- array( 'href' => $url ),
- $wgLang->timeAndDate( $timestamp, true )
- );
- $row .= '</td>';
+ if( $file->getTimestamp() === $this->img->getTimestamp() ) {
+ $selected = "class='filehistory-selected'";
+ }
+ $row .= "<td $selected style='white-space: nowrap;'>";
+ if( !$file->userCan(File::DELETED_FILE) ) {
+ # Don't link to unviewable files
+ $row .= '<span class="history-deleted">' . $wgLang->timeAndDate( $timestamp, true ) . '</span>';
+ } else if( $file->isDeleted(File::DELETED_FILE) ) {
+ $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
+ # Make a link to review the image
+ $url = $this->skin->makeKnownLinkObj( $revdel, $wgLang->timeAndDate( $timestamp, true ),
+ "target=".$wgTitle->getPrefixedText()."&file=$sha1.".$this->current->getExtension() );
+ $row .= '<span class="history-deleted">'.$url.'</span>';
+ } else {
+ $url = $iscur ? $this->current->getUrl() : $this->current->getArchiveUrl( $img );
+ $row .= Xml::element( 'a', array( 'href' => $url ), $wgLang->timeAndDate( $timestamp, true ) );
+ }
+
+ $row .= "</td><td>";
+
+ // Image dimensions
+ $row .= htmlspecialchars( $dims );
+
+ // File size
+ $row .= " <span style='white-space: nowrap;'>(" . $this->skin->formatSize( $size ) . ')</span>';
// Uploading user
- $row .= '<td>';
+ $row .= '</td><td>';
if( $local ) {
- $row .= $this->skin->userLink( $user, $usertext ) . $this->skin->userToolLinks( $user, $usertext );
+ // Hide deleted usernames
+ if( $file->isDeleted(File::DELETED_USER) ) {
+ $row .= '<span class="history-deleted">' . wfMsgHtml( 'rev-deleted-user' ) . '</span>';
+ } else {
+ $row .= $this->skin->userLink( $user, $usertext ) . " <span style='white-space: nowrap;'>" .
+ $this->skin->userToolLinks( $user, $usertext ) . "</span>";
+ }
} else {
$row .= htmlspecialchars( $usertext );
}
- $row .= '</td>';
+ $row .= '</td><td>';
- // Image dimensions
- $row .= '<td>' . htmlspecialchars( $dims ) . '</td>';
-
- // File size
- $row .= '<td class="mw-imagepage-filesize">' . $this->skin->formatSize( $size ) . '</td>';
+ // Don't show deleted descriptions
+ if ( $file->isDeleted(File::DELETED_COMMENT) ) {
+ $row .= '<span class="history-deleted">' . wfMsgHtml('rev-deleted-comment') . '</span>';
+ } else {
+ $row .= $this->skin->commentBlock( $description, $this->title );
+ }
+ $row .= '</td>';
- // Comment
- $row .= '<td>' . $this->skin->formatComment( $description, $this->title ) . '</td>';
+ wfRunHooks( 'ImagePageFileHistoryLine', array( $this, $file, &$row, &$rowClass ) );
+ $classAttr = $rowClass ? " class='$rowClass'" : "";
- return "<tr>{$row}</tr>\n";
+ return "<tr{$classAttr}>{$row}</tr>\n";
}
-
}
diff --git a/includes/ImageQueryPage.php b/includes/ImageQueryPage.php
index 8948ddc6..da9b6fd6 100644
--- a/includes/ImageQueryPage.php
+++ b/includes/ImageQueryPage.php
@@ -4,8 +4,7 @@
* Variant of QueryPage which uses a gallery to output results, thus
* suited for reports generating images
*
- * @package MediaWiki
- * @addtogroup SpecialPage
+ * @ingroup SpecialPage
* @author Rob Church <robchur@gmail.com>
*/
class ImageQueryPage extends QueryPage {
@@ -64,5 +63,3 @@ class ImageQueryPage extends QueryPage {
}
}
-
-
diff --git a/includes/JobQueue.php b/includes/JobQueue.php
index 5cec3106..8bfd1b3e 100644
--- a/includes/JobQueue.php
+++ b/includes/JobQueue.php
@@ -1,4 +1,7 @@
<?php
+/**
+ * @defgroup JobQueue JobQueue
+ */
if ( !defined( 'MEDIAWIKI' ) ) {
die( "This file is part of MediaWiki, it is not a valid entry point\n" );
@@ -6,6 +9,8 @@ if ( !defined( 'MEDIAWIKI' ) ) {
/**
* Class to both describe a background job and handle jobs.
+ *
+ * @ingroup JobQueue
*/
abstract class Job {
var $command,
@@ -37,8 +42,8 @@ abstract class Job {
*/
/**
- * Pop a job of a certain type. This tries less hard than pop() to
- * actually find a job; it may be adversely affected by concurrent job
+ * Pop a job of a certain type. This tries less hard than pop() to
+ * actually find a job; it may be adversely affected by concurrent job
* runners.
*/
static function pop_type($type) {
@@ -78,7 +83,7 @@ abstract class Job {
/**
* Pop a job off the front of the queue
- * @static
+ *
* @param $offset Number of jobs to skip
* @return Job or false if there's no jobs
*/
@@ -87,11 +92,11 @@ abstract class Job {
$dbr = wfGetDB( DB_SLAVE );
- /* Get a job from the slave, start with an offset,
+ /* Get a job from the slave, start with an offset,
scan full set afterwards, avoid hitting purged rows
- NB: If random fetch previously was used, offset
- will always be ahead of few entries
+ NB: If random fetch previously was used, offset
+ will always be ahead of few entries
*/
$row = $dbr->selectRow( 'job', '*', "job_id >= ${offset}", __METHOD__,
@@ -158,7 +163,10 @@ abstract class Job {
$job = Job::factory( $row->job_cmd, $title, Job::extractBlob( $row->job_params ), $row->job_id );
// Remove any duplicates it may have later in the queue
+ // Deadlock prone section
+ $dbw->begin();
$dbw->delete( 'job', $job->insertFields(), __METHOD__ );
+ $dbw->commit();
wfProfileOut( __METHOD__ );
return $job;
@@ -167,10 +175,10 @@ abstract class Job {
/**
* Create the appropriate object to handle a specific job
*
- * @param string $command Job command
- * @param Title $title Associated title
- * @param array $params Job parameters
- * @param int $id Job identifier
+ * @param $command String: Job command
+ * @param $title Title: Associated title
+ * @param $params Array: Job parameters
+ * @param $id Int: Job identifier
* @return Job
*/
static function factory( $command, $title, $params = false, $id = 0 ) {
@@ -181,7 +189,7 @@ abstract class Job {
}
throw new MWException( "Invalid job command `{$command}`" );
}
-
+
static function makeBlob( $params ) {
if ( $params !== false ) {
return serialize( $params );
@@ -208,12 +216,23 @@ abstract class Job {
* @param $jobs array of Job objects
*/
static function batchInsert( $jobs ) {
- if( count( $jobs ) ) {
- $dbw = wfGetDB( DB_MASTER );
- $dbw->begin();
- foreach( $jobs as $job ) {
- $rows[] = $job->insertFields();
+ if( !count( $jobs ) ) {
+ return;
+ }
+ $dbw = wfGetDB( DB_MASTER );
+ $rows = array();
+ foreach( $jobs as $job ) {
+ $rows[] = $job->insertFields();
+ if ( count( $rows ) >= 50 ) {
+ # Do a small transaction to avoid slave lag
+ $dbw->begin();
+ $dbw->insert( 'job', $rows, __METHOD__, 'IGNORE' );
+ $dbw->commit();
+ $rows = array();
}
+ }
+ if ( $rows ) {
+ $dbw->begin();
$dbw->insert( 'job', $rows, __METHOD__, 'IGNORE' );
$dbw->commit();
}
@@ -283,9 +302,11 @@ abstract class Job {
}
}
+ protected function setLastError( $error ) {
+ $this->error = $error;
+ }
+
function getLastError() {
return $this->error;
}
}
-
-
diff --git a/includes/Licenses.php b/includes/Licenses.php
index 6a034468..e76ac23c 100644
--- a/includes/Licenses.php
+++ b/includes/Licenses.php
@@ -1,8 +1,8 @@
<?php
/**
* A License class for use on Special:Upload
- *
- * @addtogroup SpecialPage
+ *
+ * @ingroup SpecialPage
*
* @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
* @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason
@@ -172,4 +172,3 @@ class License {
$this->text = strrev( $text );
}
}
-
diff --git a/includes/LinkBatch.php b/includes/LinkBatch.php
index db1114c9..bdc4b43a 100644
--- a/includes/LinkBatch.php
+++ b/includes/LinkBatch.php
@@ -4,7 +4,7 @@
* Class representing a list of titles
* The execute() method checks them all for existence and adds them to a LinkCache object
*
- * @addtogroup Cache
+ * @ingroup Cache
*/
class LinkBatch {
/**
@@ -18,7 +18,7 @@ class LinkBatch {
}
}
- function addObj( $title ) {
+ public function addObj( $title ) {
if ( is_object( $title ) ) {
$this->add( $title->getNamespace(), $title->getDBkey() );
} else {
@@ -26,7 +26,7 @@ class LinkBatch {
}
}
- function add( $ns, $dbkey ) {
+ public function add( $ns, $dbkey ) {
if ( $ns < 0 ) {
return;
}
@@ -41,21 +41,21 @@ class LinkBatch {
* Set the link list to a given 2-d array
* First key is the namespace, second is the DB key, value arbitrary
*/
- function setArray( $array ) {
+ public function setArray( $array ) {
$this->data = $array;
}
/**
* Returns true if no pages have been added, false otherwise.
*/
- function isEmpty() {
+ public function isEmpty() {
return ($this->getSize() == 0);
}
/**
* Returns the size of the batch.
*/
- function getSize() {
+ public function getSize() {
return count( $this->data );
}
@@ -63,8 +63,8 @@ class LinkBatch {
* Do the query and add the results to the LinkCache object
* Return an array mapping PDBK to ID
*/
- function execute() {
- $linkCache =& LinkCache::singleton();
+ public function execute() {
+ $linkCache = LinkCache::singleton();
return $this->executeInto( $linkCache );
}
@@ -72,13 +72,22 @@ class LinkBatch {
* Do the query and add the results to a given LinkCache object
* Return an array mapping PDBK to ID
*/
- function executeInto( &$cache ) {
- $fname = 'LinkBatch::executeInto';
- wfProfileIn( $fname );
- // Do query
+ protected function executeInto( &$cache ) {
+ wfProfileIn( __METHOD__ );
$res = $this->doQuery();
+ $ids = $this->addResultToCache( $cache, $res );
+ wfProfileOut( __METHOD__ );
+ return $ids;
+ }
+
+ /**
+ * Add a ResultWrapper containing IDs and titles to a LinkCache object.
+ * As normal, titles will go into the static Title cache field.
+ * This function *also* stores extra fields of the title used for link
+ * parsing to avoid extra DB queries.
+ */
+ public function addResultToCache( $cache, $res ) {
if ( !$res ) {
- wfProfileOut( $fname );
return array();
}
@@ -88,11 +97,10 @@ class LinkBatch {
$remaining = $this->data;
while ( $row = $res->fetchObject() ) {
$title = Title::makeTitle( $row->page_namespace, $row->page_title );
- $cache->addGoodLinkObj( $row->page_id, $title );
+ $cache->addGoodLinkObj( $row->page_id, $title, $row->page_len, $row->page_is_redirect );
$ids[$title->getPrefixedDBkey()] = $row->page_id;
unset( $remaining[$row->page_namespace][$row->page_title] );
}
- $res->free();
// The remaining links in $data are bad links, register them as such
foreach ( $remaining as $ns => $dbkeys ) {
@@ -102,20 +110,17 @@ class LinkBatch {
$ids[$title->getPrefixedDBkey()] = 0;
}
}
- wfProfileOut( $fname );
return $ids;
}
/**
* Perform the existence test query, return a ResultWrapper with page_id fields
*/
- function doQuery() {
- $fname = 'LinkBatch::doQuery';
-
+ public function doQuery() {
if ( $this->isEmpty() ) {
return false;
}
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
// Construct query
// This is very similar to Parser::replaceLinkHolders
@@ -123,26 +128,25 @@ class LinkBatch {
$page = $dbr->tableName( 'page' );
$set = $this->constructSet( 'page', $dbr );
if ( $set === false ) {
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return false;
}
- $sql = "SELECT page_id, page_namespace, page_title FROM $page WHERE $set";
+ $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, $fname ) );
- wfProfileOut( $fname );
+ $res = new ResultWrapper( $dbr, $dbr->query( $sql, __METHOD__ ) );
+ wfProfileOut( __METHOD__ );
return $res;
}
/**
* Construct a WHERE clause which will match all the given titles.
- * Give the appropriate table's field name prefix ('page', 'pl', etc).
*
- * @param $prefix String: ??
+ * @param string $prefix the appropriate table's field name prefix ('page', 'pl', etc)
* @return string
* @public
*/
- function constructSet( $prefix, &$db ) {
+ public function constructSet( $prefix, &$db ) {
$first = true;
$firstTitle = true;
$sql = '';
@@ -156,7 +160,7 @@ class LinkBatch {
} else {
$sql .= ' OR ';
}
-
+
if (count($dbkeys)==1) { // avoid multiple-reference syntax if simple equality can be used
$singleKey = array_keys($dbkeys);
$sql .= "({$prefix}_namespace=$ns AND {$prefix}_title=".
@@ -164,7 +168,7 @@ class LinkBatch {
")";
} else {
$sql .= "({$prefix}_namespace=$ns AND {$prefix}_title IN (";
-
+
$firstTitle = true;
foreach( $dbkeys as $dbkey => $unused ) {
if ( $firstTitle ) {
@@ -185,5 +189,3 @@ class LinkBatch {
}
}
}
-
-
diff --git a/includes/LinkCache.php b/includes/LinkCache.php
index 7c49d88e..79727615 100644
--- a/includes/LinkCache.php
+++ b/includes/LinkCache.php
@@ -1,13 +1,13 @@
<?php
/**
* Cache for article titles (prefixed DB keys) and ids linked from one source
- *
- * @addtogroup Cache
+ *
+ * @ingroup Cache
*/
class LinkCache {
// Increment $mClassVer whenever old serialized versions of this class
// becomes incompatible with the new version.
- /* private */ var $mClassVer = 3;
+ /* private */ var $mClassVer = 4;
/* private */ var $mPageLinks;
/* private */ var $mGoodLinks, $mBadLinks;
@@ -28,21 +28,18 @@ class LinkCache {
$this->mForUpdate = false;
$this->mPageLinks = array();
$this->mGoodLinks = array();
+ $this->mGoodLinkFields = array();
$this->mBadLinks = array();
}
- /* private */ function getKey( $title ) {
- return wfMemcKey( 'lc', 'title', $title );
- }
-
/**
* General accessor to get/set whether SELECT FOR UPDATE should be used
*/
- function forUpdate( $update = NULL ) {
+ public function forUpdate( $update = NULL ) {
return wfSetVar( $this->mForUpdate, $update );
}
- function getGoodLinkID( $title ) {
+ public function getGoodLinkID( $title ) {
if ( array_key_exists( $title, $this->mGoodLinks ) ) {
return $this->mGoodLinks[$title];
} else {
@@ -50,17 +47,41 @@ class LinkCache {
}
}
- function isBadLink( $title ) {
+ /**
+ * Get a field of a title object from cache.
+ * If this link is not good, it will return NULL.
+ * @param Title $title
+ * @param string $field ('length','redirect')
+ * @return mixed
+ */
+ public function getGoodLinkFieldObj( $title, $field ) {
+ $dbkey = $title->getPrefixedDbKey();
+ if ( array_key_exists( $dbkey, $this->mGoodLinkFields ) ) {
+ return $this->mGoodLinkFields[$dbkey][$field];
+ } else {
+ return NULL;
+ }
+ }
+
+ public function isBadLink( $title ) {
return array_key_exists( $title, $this->mBadLinks );
}
- function addGoodLinkObj( $id, $title ) {
+ /**
+ * Add a link for the title to the link cache
+ * @param int $id
+ * @param Title $title
+ * @param int $len
+ * @param int $redir
+ */
+ public function addGoodLinkObj( $id, $title, $len = -1, $redir = NULL ) {
$dbkey = $title->getPrefixedDbKey();
$this->mGoodLinks[$dbkey] = $id;
+ $this->mGoodLinkFields[$dbkey] = array( 'length' => $len, 'redirect' => $redir );
$this->mPageLinks[$dbkey] = $title;
}
- function addBadLinkObj( $title ) {
+ public function addBadLinkObj( $title ) {
$dbkey = $title->getPrefixedDbKey();
if ( ! $this->isBadLink( $dbkey ) ) {
$this->mBadLinks[$dbkey] = 1;
@@ -68,30 +89,28 @@ class LinkCache {
}
}
- function clearBadLink( $title ) {
+ public function clearBadLink( $title ) {
unset( $this->mBadLinks[$title] );
- $this->clearLink( $title );
}
- function clearLink( $title ) {
- global $wgMemc, $wgLinkCacheMemcached;
- if( $wgLinkCacheMemcached )
- $wgMemc->delete( $this->getKey( $title ) );
- }
+ /* obsolete, for old $wgLinkCacheMemcached stuff */
+ public function clearLink( $title ) {}
- function getPageLinks() { return $this->mPageLinks; }
- function getGoodLinks() { return $this->mGoodLinks; }
- function getBadLinks() { return array_keys( $this->mBadLinks ); }
+ public function getPageLinks() { return $this->mPageLinks; }
+ public function getGoodLinks() { return $this->mGoodLinks; }
+ public function getBadLinks() { return array_keys( $this->mBadLinks ); }
/**
* Add a title to the link cache, return the page_id or zero if non-existent
* @param $title String: title to add
+ * @param $len int, page size
+ * @param $redir bool, is redirect?
* @return integer
*/
- function addLink( $title ) {
+ public function addLink( $title, $len = -1, $redir = NULL ) {
$nt = Title::newFromDBkey( $title );
if( $nt ) {
- return $this->addLinkObj( $nt );
+ return $this->addLinkObj( $nt, $len, $redir );
} else {
return 0;
}
@@ -100,18 +119,20 @@ class LinkCache {
/**
* Add a title to the link cache, return the page_id or zero if non-existent
* @param $nt Title to add.
+ * @param $len int, page size
+ * @param $redir bool, is redirect?
* @return integer
*/
- function addLinkObj( &$nt ) {
- global $wgMemc, $wgLinkCacheMemcached, $wgAntiLockFlags;
+ public function addLinkObj( &$nt, $len = -1, $redirect = NULL ) {
+ global $wgAntiLockFlags, $wgProfiler;
+
$title = $nt->getPrefixedDBkey();
if ( $this->isBadLink( $title ) ) { return 0; }
$id = $this->getGoodLinkID( $title );
if ( 0 != $id ) { return $id; }
$fname = 'LinkCache::addLinkObj';
- global $wgProfiling, $wgProfiler;
- if ( $wgProfiling && isset( $wgProfiler ) ) {
+ if ( isset( $wgProfiler ) ) {
$fname .= ' (' . $wgProfiler->getCurrentSection() . ')';
}
@@ -125,36 +146,32 @@ class LinkCache {
return 0;
}
- $id = NULL;
- if( $wgLinkCacheMemcached )
- $id = $wgMemc->get( $key = $this->getKey( $title ) );
- if( ! is_integer( $id ) ) {
- if ( $this->mForUpdate ) {
- $db = wfGetDB( DB_MASTER );
- if ( !( $wgAntiLockFlags & ALF_NO_LINK_LOCK ) ) {
- $options = array( 'FOR UPDATE' );
- } else {
- $options = array();
- }
+ # Some fields heavily used for linking...
+ if ( $this->mForUpdate ) {
+ $db = wfGetDB( DB_MASTER );
+ if ( !( $wgAntiLockFlags & ALF_NO_LINK_LOCK ) ) {
+ $options = array( 'FOR UPDATE' );
} else {
- $db = wfGetDB( DB_SLAVE );
$options = array();
}
-
- $id = $db->selectField( 'page', 'page_id',
- array( 'page_namespace' => $ns, 'page_title' => $t ),
- $fname, $options );
- if ( !$id ) {
- $id = 0;
- }
- if( $wgLinkCacheMemcached )
- $wgMemc->add( $key, $id, 3600*24 );
+ } else {
+ $db = wfGetDB( DB_SLAVE );
+ $options = array();
}
+ $s = $db->selectRow( 'page',
+ array( 'page_id', 'page_len', 'page_is_redirect' ),
+ array( 'page_namespace' => $ns, 'page_title' => $t ),
+ $fname, $options );
+ # Set fields...
+ $id = $s ? $s->page_id : 0;
+ $len = $s ? $s->page_len : -1;
+ $redirect = $s ? $s->page_is_redirect : 0;
+
if( 0 == $id ) {
$this->addBadLinkObj( $nt );
} else {
- $this->addGoodLinkObj( $id, $nt );
+ $this->addGoodLinkObj( $id, $nt, $len, $redirect );
}
wfProfileOut( $fname );
return $id;
@@ -163,10 +180,10 @@ class LinkCache {
/**
* Clears cache
*/
- function clear() {
+ public function clear() {
$this->mPageLinks = array();
$this->mGoodLinks = array();
+ $this->mGoodLinkFields = array();
$this->mBadLinks = array();
}
}
-
diff --git a/includes/LinkFilter.php b/includes/LinkFilter.php
index ced76d75..dc4c1256 100644
--- a/includes/LinkFilter.php
+++ b/includes/LinkFilter.php
@@ -106,4 +106,3 @@ class LinkFilter {
return $like;
}
}
-
diff --git a/includes/Linker.php b/includes/Linker.php
index 4b092cf9..32c506a4 100644
--- a/includes/Linker.php
+++ b/includes/Linker.php
@@ -1,15 +1,12 @@
<?php
/**
- * Split off some of the internal bits from Skin.php.
- * These functions are used for primarily page content:
- * links, embedded images, table of contents. Links are
- * also used in the skin.
- * For the moment, Skin is a descendent class of Linker.
- * In the future, it should probably be further split
- * so that ever other bit of the wiki doesn't have to
- * go loading up Skin to get at it.
+ * Split off some of the internal bits from Skin.php. These functions are used
+ * for primarily page content: links, embedded images, table of contents. Links
+ * are also used in the skin. For the moment, Skin is a descendent class of
+ * Linker. In the future, it should probably be further split so that every
+ * other bit of the wiki doesn't have to go loading up Skin to get at it.
*
- * @addtogroup Skins
+ * @ingroup Skins
*/
class Linker {
@@ -23,72 +20,110 @@ class Linker {
/**
* @deprecated
*/
- function postParseLinkColour( $s = NULL ) {
- return NULL;
+ function postParseLinkColour( $s = null ) {
+ return null;
}
- /** @todo document */
- function getExternalLinkAttributes( $link, $text, $class='' ) {
- $link = htmlspecialchars( $link );
-
- $r = ($class != '') ? " class=\"$class\"" : " class=\"external\"";
-
- $r .= " title=\"{$link}\"";
- return $r;
+ /**
+ * Get the appropriate HTML attributes to add to the "a" element of an ex-
+ * ternal link, as created by [wikisyntax].
+ *
+ * @param string $title The (unescaped) title text for the link
+ * @param string $unused Unused
+ * @param string $class The contents of the class attribute; if an empty
+ * string is passed, which is the default value, defaults to 'external'.
+ */
+ function getExternalLinkAttributes( $title, $unused = null, $class='' ) {
+ return $this->getLinkAttributesInternal( $title, $class, 'external' );
}
- function getInterwikiLinkAttributes( $link, $text, $class='' ) {
+ /**
+ * Get the appropriate HTML attributes to add to the "a" element of an in-
+ * terwiki link.
+ *
+ * @param string $title The title text for the link, URL-encoded (???) but
+ * not HTML-escaped
+ * @param string $unused Unused
+ * @param string $class The contents of the class attribute; if an empty
+ * string is passed, which is the default value, defaults to 'external'.
+ */
+ function getInterwikiLinkAttributes( $title, $unused = null, $class='' ) {
global $wgContLang;
- $link = urldecode( $link );
- $link = $wgContLang->checkTitleEncoding( $link );
- $link = preg_replace( '/[\\x00-\\x1f]/', ' ', $link );
- $link = htmlspecialchars( $link );
+ # FIXME: We have a whole bunch of handling here that doesn't happen in
+ # getExternalLinkAttributes, why?
+ $title = urldecode( $title );
+ $title = $wgContLang->checkTitleEncoding( $title );
+ $title = preg_replace( '/[\\x00-\\x1f]/', ' ', $title );
- $r = ($class != '') ? " class=\"$class\"" : " class=\"external\"";
+ return $this->getLinkAttributesInternal( $title, $class, 'external' );
+ }
- $r .= " title=\"{$link}\"";
- return $r;
+ /**
+ * Get the appropriate HTML attributes to add to the "a" element of an in-
+ * ternal link.
+ *
+ * @param string $title The title text for the link, URL-encoded (???) but
+ * not HTML-escaped
+ * @param string $unused Unused
+ * @param string $class The contents of the class attribute, default none
+ */
+ function getInternalLinkAttributes( $title, $unused = null, $class='' ) {
+ $title = urldecode( $title );
+ $title = str_replace( '_', ' ', $title );
+ return $this->getLinkAttributesInternal( $title, $class );
}
- /** @todo document */
- function getInternalLinkAttributes( $link, $text, $class='' ) {
- $link = urldecode( $link );
- $link = str_replace( '_', ' ', $link );
- $link = htmlspecialchars( $link );
- $r = ($class != '') ? ' class="' . htmlspecialchars( $class ) . '"' : '';
- $r .= " title=\"{$link}\"";
- return $r;
+ /**
+ * Get the appropriate HTML attributes to add to the "a" element of an in-
+ * ternal link, given the Title object for the page we want to link to.
+ *
+ * @param Title $nt The Title object
+ * @param string $unused Unused
+ * @param string $class The contents of the class attribute, default none
+ * @param mixed $title Optional (unescaped) string to use in the title
+ * attribute; if false, default to the name of the page we're linking to
+ */
+ function getInternalLinkAttributesObj( $nt, $unused = null, $class = '', $title = false ) {
+ if( $title === false ) {
+ $title = $nt->getPrefixedText();
+ }
+ return $this->getLinkAttributesInternal( $title, $class );
}
/**
- * @param $nt Title object.
- * @param $text String: FIXME
- * @param $class String: CSS class of the link, default ''.
+ * Common code for getLinkAttributesX functions
*/
- function getInternalLinkAttributesObj( &$nt, $text, $class='' ) {
- $r = ($class != '') ? ' class="' . htmlspecialchars( $class ) . '"' : '';
- $r .= ' title="' . $nt->getEscapedText() . '"';
+ private function getLinkAttributesInternal( $title, $class, $classDefault = false ) {
+ $title = htmlspecialchars( $title );
+ if( $class === '' and $classDefault !== false ) {
+ # FIXME: Parameter defaults the hard way! We should just have
+ # $class = 'external' or whatever as the default in the externally-
+ # exposed functions, not $class = ''.
+ $class = $classDefault;
+ }
+ $class = htmlspecialchars( $class );
+ $r = '';
+ if( $class !== '' ) {
+ $r .= " class=\"$class\"";
+ }
+ $r .= " title=\"$title\"";
return $r;
}
/**
* Return the CSS colour of a known link
*
- * @param mixed $s
+ * @param Title $t
* @param integer $threshold user defined threshold
* @return string CSS class
*/
- function getLinkColour( $s, $threshold ) {
- if( $s === false ) {
- return '';
- }
-
+ function getLinkColour( $t, $threshold ) {
$colour = '';
- if ( !empty( $s->page_is_redirect ) ) {
+ if ( $t->isRedirect() ) {
# Page is a redirect
$colour = 'mw-redirect';
- } elseif ( $threshold > 0 && $s->page_len < $threshold && Namespace::isContent( $s->page_namespace ) ) {
+ } elseif ( $threshold > 0 && $t->getLength() < $threshold && MWNamespace::isContent( $t->getNamespace() ) ) {
# Page is a stub
$colour = 'stub';
}
@@ -123,7 +158,7 @@ class Linker {
/**
* 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.
- *
+ *
* @param $title String: the text of the title
* @param $text String: link text
* @param $query String: optional query part
@@ -144,7 +179,7 @@ class Linker {
/**
* 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.
- *
+ *
* @param string $title The text of the title
* @param string $text Link text
* @param string $query Optional query part
@@ -164,10 +199,10 @@ class Linker {
/**
* @deprecated use makeColouredLinkObj
- *
+ *
* 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.
- *
+ *
* @param $title String: the text of the title
* @param $text String: link text
* @param $query String: optional query part
@@ -189,7 +224,7 @@ class Linker {
* 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.
- *
+ *
* @param $nt Title: the title object to make the link from, e.g. from
* Title::newFromText.
* @param $text String: link text
@@ -228,7 +263,7 @@ class Linker {
wfProfileOut( __METHOD__ );
return $t;
} elseif ( $nt->isAlwaysKnown() ) {
- # Image links, special page links and self-links with fragements are always known.
+ # 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' );
@@ -252,15 +287,8 @@ class Linker {
} else {
$colour = '';
if ( $nt->isContentPage() ) {
- # FIXME: This is stupid, we should combine this query with
- # the Title::getArticleID() query above.
$threshold = $wgUser->getOption('stubthreshold');
- $dbr = wfGetDB( DB_SLAVE );
- $s = $dbr->selectRow(
- array( 'page' ),
- array( 'page_len', 'page_is_redirect', 'page_namespace' ),
- array( 'page_id' => $aid ), __METHOD__ ) ;
- $colour = $this->getLinkColour( $s, $threshold );
+ $colour = $this->getLinkColour( $nt, $threshold );
}
$retVal = $this->makeColouredLinkObj( $nt, $colour, $text, $query, $trail, $prefix );
}
@@ -284,15 +312,17 @@ class Linker {
* @param $style String: style to apply - if empty, use getInternalLinkAttributesObj instead
* @return the a-element
*/
- function makeKnownLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' , $aprops = '', $style = '' ) {
+ function makeKnownLinkObj( $title, $text = '', $query = '', $trail = '', $prefix = '' , $aprops = '', $style = '' ) {
wfProfileIn( __METHOD__ );
- if ( !$nt instanceof Title ) {
+ 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() == '' ) {
@@ -320,7 +350,7 @@ class Linker {
/**
* Make a red link to the edit page of a given title.
- *
+ *
* @param $nt Title object of the target page
* @param $text String: Link text
* @param $query String: Optional query part
@@ -328,30 +358,35 @@ class Linker {
* be included in the link text. Other characters will be appended after
* the end of the link.
*/
- function makeBrokenLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
+ function makeBrokenLinkObj( $title, $text = '', $query = '', $trail = '', $prefix = '' ) {
wfProfileIn( __METHOD__ );
- if ( !$nt instanceof Title ) {
+ if ( !$title instanceof Title ) {
# Fail gracefully
wfProfileOut( __METHOD__ );
return "<!-- ERROR -->{$prefix}{$text}{$trail}";
}
+ $nt = $this->normaliseSpecialPage( $title );
+
if( $nt->getNamespace() == NS_SPECIAL ) {
$q = $query;
} else if ( '' == $query ) {
- $q = 'action=edit';
+ $q = 'action=edit&redlink=1';
} else {
- $q = 'action=edit&'.$query;
+ $q = 'action=edit&redlink=1&'.$query;
}
$u = $nt->escapeLocalURL( $q );
+ $titleText = $nt->getPrefixedText();
if ( '' == $text ) {
- $text = htmlspecialchars( $nt->getPrefixedText() );
+ $text = htmlspecialchars( $titleText );
}
- $style = $this->getInternalLinkAttributesObj( $nt, $text, 'new' );
-
+ $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}";
wfProfileOut( __METHOD__ );
@@ -360,9 +395,9 @@ class Linker {
/**
* @deprecated use makeColouredLinkObj
- *
+ *
* Make a brown link to a short article.
- *
+ *
* @param $nt Title object of the target page
* @param $text String: link text
* @param $query String: optional query part
@@ -376,7 +411,7 @@ class Linker {
/**
* Make a coloured link.
- *
+ *
* @param $nt Title object of the target page
* @param $colour Integer: colour of the link
* @param $text String: link text
@@ -412,7 +447,7 @@ class Linker {
return $this->makeColouredLinkObj( $nt, $colour, $text, $query, $trail, $prefix );
}
- /**
+ /**
* Make appropriate markup for a link to the current article. This is currently rendered
* as the bold link text. The calling sequence is the same as the other make*LinkObj functions,
* despite $query not being used.
@@ -425,6 +460,16 @@ class Linker {
return "<strong class=\"selflink\">{$prefix}{$text}{$inside}</strong>{$trail}";
}
+ function normaliseSpecialPage( Title $title ) {
+ if ( $title->getNamespace() == NS_SPECIAL ) {
+ list( $name, $subpage ) = SpecialPage::resolveAliasWithSubpage( $title->getDBkey() );
+ if ( !$name ) return $title;
+ return SpecialPage::getTitleFor( $name, $subpage );
+ } else {
+ return $title;
+ }
+ }
+
/** @todo document */
function fnamePart( $url ) {
$basename = strrchr( $url, '/' );
@@ -433,7 +478,7 @@ class Linker {
} else {
$basename = substr( $basename, 1 );
}
- return htmlspecialchars( $basename );
+ return $basename;
}
/** Obsolete alias */
@@ -446,11 +491,19 @@ class Linker {
if ( '' == $alt ) {
$alt = $this->fnamePart( $url );
}
- $s = '<img src="'.$url.'" alt="'.$alt.'" />';
- return $s;
+ $img = '';
+ $success = wfRunHooks('LinkerMakeExternalImage', array( &$url, &$alt, &$img ) );
+ if(!$success) {
+ wfDebug("Hook LinkerMakeExternalImage changed the output of external image with url {$url} and alt text {$alt} to {$img}", true);
+ return $img;
+ }
+ return Xml::element( 'img',
+ array(
+ 'src' => $url,
+ 'alt' => $alt ) );
}
- /**
+ /**
* Creates the HTML source for images
* @deprecated use makeImageLink2
*
@@ -490,12 +543,14 @@ class Linker {
}
/**
- * Make an image link
+ * Given parameters derived from [[Image:Foo|options...]], generate the
+ * HTML that that syntax inserts in the page.
+ *
* @param Title $title Title object
* @param File $file File object, or false if it doesn't exist
*
* @param array $frameParams Associative array of parameters external to the media handler.
- * Boolean parameters are indicated by presence or absence, the value is arbitrary and
+ * Boolean parameters are indicated by presence or absence, the value is arbitrary and
* will often be false.
* thumbnail If present, downscale and frame
* manualthumb Image name to use as a thumbnail, instead of automatic scaling
@@ -505,16 +560,24 @@ class Linker {
* upright_factor Fudge factor for "upright" tweak (default 0.75)
* border If present, show a border around the image
* align Horizontal alignment (left, right, center, none)
- * valign Vertical alignment (baseline, sub, super, top, text-top, middle,
+ * valign Vertical alignment (baseline, sub, super, top, text-top, middle,
* bottom, text-bottom)
* alt Alternate text for image (i.e. alt attribute). Plain text.
* caption HTML for image caption.
*
- * @param array $handlerParams Associative array of media handler parameters, to be passed
- * to transform(). Typical keys are "width" and "page".
+ * @param array $handlerParams Associative array of media handler parameters, to be passed
+ * to transform(). Typical keys are "width" and "page".
* @param string $time, timestamp of the file, set as false for current
+ * @param string $query, query params for desc url
+ * @return string HTML for an image, with links, wrappers, etc.
*/
- function makeImageLink2( Title $title, $file, $frameParams = array(), $handlerParams = array(), $time = false ) {
+ function makeImageLink2( Title $title, $file, $frameParams = array(), $handlerParams = array(), $time = false, $query = "" ) {
+ $res = null;
+ if( !wfRunHooks( 'ImageBeforeProduceHTML', array( &$this, &$title,
+ &$file, &$frameParams, &$handlerParams, &$time, &$res ) ) ) {
+ return $res;
+ }
+
global $wgContLang, $wgUser, $wgThumbLimits, $wgThumbUpright;
if ( $file && !$file->allowInlineDisplay() ) {
wfDebug( __METHOD__.': '.$title->getPrefixedDBkey()." does not allow inline display\n" );
@@ -554,8 +617,8 @@ class Linker {
}
// Use width which is smaller: real image width or user preference width
// For caching health: If width scaled down due to upright parameter, round to full __0 pixel to avoid the creation of a lot of odd thumbs
- $prefWidth = isset( $fp['upright'] ) ?
- round( $wgThumbLimits[$wopt] * $fp['upright'], -1 ) :
+ $prefWidth = isset( $fp['upright'] ) ?
+ round( $wgThumbLimits[$wopt] * $fp['upright'], -1 ) :
$wgThumbLimits[$wopt];
if ( $hp['width'] <= 0 || $prefWidth < $hp['width'] ) {
$hp['width'] = $prefWidth;
@@ -575,7 +638,7 @@ class Linker {
if ( $fp['align'] == '' ) {
$fp['align'] = $wgContLang->isRTL() ? 'left' : 'right';
}
- return $prefix.$this->makeThumbLink2( $title, $file, $fp, $hp, $time ).$postfix;
+ return $prefix.$this->makeThumbLink2( $title, $file, $fp, $hp, $time, $query ).$postfix;
}
if ( $file && isset( $fp['frameless'] ) ) {
@@ -599,6 +662,7 @@ class Linker {
} else {
$s = $thumb->toHtml( array(
'desc-link' => true,
+ 'desc-query' => $query,
'alt' => $fp['alt'],
'valign' => isset( $fp['valign'] ) ? $fp['valign'] : false ,
'img-class' => isset( $fp['border'] ) ? 'thumbborder' : false ) );
@@ -611,11 +675,11 @@ class Linker {
/**
* Make HTML for a thumbnail including image, border and caption
- * @param Title $title
+ * @param Title $title
* @param File $file File object or false if it doesn't exist
*/
function makeThumbLinkObj( Title $title, $file, $label = '', $alt, $align = 'right', $params = array(), $framed=false , $manualthumb = "" ) {
- $frameParams = array(
+ $frameParams = array(
'alt' => $alt,
'caption' => $label,
'align' => $align
@@ -625,7 +689,7 @@ class Linker {
return $this->makeThumbLink2( $title, $file, $frameParams, $params );
}
- function makeThumbLink2( Title $title, $file, $frameParams = array(), $handlerParams = array(), $time = false ) {
+ function makeThumbLink2( Title $title, $file, $frameParams = array(), $handlerParams = array(), $time = false, $query = "" ) {
global $wgStylePath, $wgContLang;
$exists = $file && $file->exists();
@@ -639,7 +703,7 @@ class Linker {
if ( !isset( $fp['caption'] ) ) $fp['caption'] = '';
if ( empty( $hp['width'] ) ) {
- // Reduce width for upright images when parameter 'upright' is used
+ // Reduce width for upright images when parameter 'upright' is used
$hp['width'] = isset( $fp['upright'] ) ? 130 : 180;
}
$thumb = false;
@@ -678,7 +742,9 @@ class Linker {
}
}
- $query = $page ? 'page=' . urlencode( $page ) : '';
+ if( $page ) {
+ $query = $query ? '&page=' . urlencode( $page ) : 'page=' . urlencode( $page );
+ }
$url = $title->getLocalURL( $query );
$more = htmlspecialchars( wfMsg( 'thumbnail-more' ) );
@@ -694,7 +760,8 @@ class Linker {
$s .= $thumb->toHtml( array(
'alt' => $fp['alt'],
'img-class' => 'thumbimage',
- 'desc-link' => true ) );
+ 'desc-link' => true,
+ 'desc-query' => $query ) );
if ( isset( $fp['framed'] ) ) {
$zoomicon="";
} else {
@@ -729,9 +796,9 @@ class Linker {
if( $text == '' )
$text = htmlspecialchars( $title->getPrefixedText() );
$redir = RepoGroup::singleton()->getLocalRepo()->checkRedirect( $title );
- if( $redir ) {
+ if( $redir ) {
return $this->makeKnownLinkObj( $title, $text, $query, $trail, $prefix );
- }
+ }
$q = 'wpDestFile=' . $title->getPartialUrl();
if( $query != '' )
$q .= '&' . $query;
@@ -750,27 +817,28 @@ class Linker {
}
/** @deprecated use Linker::makeMediaLinkObj() */
- function makeMediaLink( $name, $unused = '', $text = '' ) {
+ function makeMediaLink( $name, $unused = '', $text = '', $time = false ) {
$nt = Title::makeTitleSafe( NS_IMAGE, $name );
- return $this->makeMediaLinkObj( $nt, $text );
+ return $this->makeMediaLinkObj( $nt, $text, $time );
}
/**
* Create a direct link to a given uploaded file.
*
* @param $title Title object.
- * @param $text String: pre-sanitized HTML
+ * @param $text String: pre-sanitized HTML
+ * @param $time string: time image was created
* @return string HTML
*
* @public
* @todo Handle invalid or missing images better.
*/
- function makeMediaLinkObj( $title, $text = '' ) {
+ function makeMediaLinkObj( $title, $text = '', $time = false ) {
if( is_null( $title ) ) {
### HOTFIX. Instead of breaking, return empty string.
return $text;
} else {
- $img = wfFindFile( $title );
+ $img = wfFindFile( $title, $time );
if( $img ) {
$url = $img->getURL();
$class = 'internal';
@@ -809,6 +877,12 @@ class Linker {
if( $escape ) {
$text = htmlspecialchars( $text );
}
+ $link = '';
+ $success = wfRunHooks('LinkerMakeExternalLink', array( &$url, &$text, &$link ) );
+ if(!$success) {
+ 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>';
}
@@ -838,9 +912,10 @@ class Linker {
* @param string $userText User name or IP address
* @param bool $redContribsWhenNoEdits Should the contributions link be red if the user has no edits?
* @param int $flags Customisation flags (e.g. self::TOOL_LINKS_NOBLOCK)
+ * @param int $edits, user edit count (optional, for performance)
* @return string
*/
- public function userToolLinks( $userId, $userText, $redContribsWhenNoEdits = false, $flags = 0 ) {
+ public function userToolLinks( $userId, $userText, $redContribsWhenNoEdits = false, $flags = 0, $edits=null ) {
global $wgUser, $wgDisableAnonTalk, $wgSysopUserBans;
$talkable = !( $wgDisableAnonTalk && 0 == $userId );
$blockable = ( $wgSysopUserBans || 0 == $userId ) && !$flags & self::TOOL_LINKS_NOBLOCK;
@@ -851,8 +926,9 @@ class Linker {
}
if( $userId ) {
// check if the user has an edit
- if( $redContribsWhenNoEdits && User::edits( $userId ) == 0 ) {
- $style = " class='new'";
+ if( $redContribsWhenNoEdits ) {
+ $count = !is_null($edits) ? $edits : User::edits( $userId );
+ $style = ($count == 0) ? " class='new'" : '';
} else {
$style = '';
}
@@ -873,9 +949,12 @@ class Linker {
/**
* Alias for userToolLinks( $userId, $userText, true );
+ * @param int $userId User identifier
+ * @param string $userText User name or IP address
+ * @param int $edits, user edit count (optional, for performance)
*/
- public function userToolLinksRedContribs( $userId, $userText ) {
- return $this->userToolLinks( $userId, $userText, true );
+ public function userToolLinksRedContribs( $userId, $userText, $edits=null ) {
+ return $this->userToolLinks( $userId, $userText, true, 0, $edits );
}
@@ -903,14 +982,17 @@ class Linker {
wfMsgHtml( 'blocklink' ) );
return $blockLink;
}
-
+
/**
* Generate a user link if the current user is allowed to view it
* @param $rev Revision object.
+ * @param $isPublic, bool, show only if all users can see it
* @return string HTML
*/
- function revUserLink( $rev ) {
- if( $rev->userCan( Revision::DELETED_USER ) ) {
+ function revUserLink( $rev, $isPublic = false ) {
+ 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() );
} else {
$link = wfMsgHtml( 'rev-deleted-user' );
@@ -924,22 +1006,24 @@ class Linker {
/**
* Generate a user tool link cluster if the current user is allowed to view it
* @param $rev Revision object.
+ * @param $isPublic, bool, show only if all users can see it
* @return string HTML
*/
- function revUserTools( $rev ) {
- if( $rev->userCan( Revision::DELETED_USER ) ) {
+ function revUserTools( $rev, $isPublic = false ) {
+ 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() );
+ ' ' . $this->userToolLinks( $rev->getRawUser(), $rev->getRawUserText() );
} else {
$link = wfMsgHtml( 'rev-deleted-user' );
}
if( $rev->isDeleted( Revision::DELETED_USER ) ) {
- return '<span class="history-deleted">' . $link . '</span>';
+ return ' <span class="history-deleted">' . $link . '</span>';
}
return $link;
}
-
+
/**
* This function is called by all recent changes variants, by the page history,
* and by the user contributions list. It is responsible for formatting edit
@@ -1012,10 +1096,12 @@ class Linker {
}
$auto = $link . $auto;
if( $pre ) {
- $auto = '- ' . $auto; # written summary $presep autocomment (summary /* section */)
+ # written summary $presep autocomment (summary /* section */)
+ $auto = wfMsgExt( 'autocomment-prefix', array( 'escapenoentities', 'content' ) ) . $auto;
}
if( $post ) {
- $auto .= ': '; # autocomment $postsep written summary (/* section */ summary)
+ # autocomment $postsep written summary (/* section */ summary)
+ $auto .= wfMsgExt( 'colon-separator', array( 'escapenoentities', 'content' ) );
}
$auto = '<span class="autocomment">' . $auto . '</span>';
$comment = $pre . $auto . $post;
@@ -1038,15 +1124,20 @@ class Linker {
array( $this, 'formatLinksInCommentCallback' ),
$comment );
}
-
+
protected function formatLinksInCommentCallback( $match ) {
global $wgContLang;
- $medians = '(?:' . preg_quote( Namespace::getCanonicalName( NS_MEDIA ), '/' ) . '|';
+ $medians = '(?:' . preg_quote( MWNamespace::getCanonicalName( NS_MEDIA ), '/' ) . '|';
$medians .= preg_quote( $wgContLang->getNsText( NS_MEDIA ), '/' ) . '):';
-
+
$comment = $match[0];
+ # fix up urlencoded title texts (copied from Parser::replaceInternalLinks)
+ if( strpos( $match[1], '%' ) !== false ) {
+ $match[1] = str_replace( array('<', '>'), array('&lt;', '&gt;'), urldecode($match[1]) );
+ }
+
# Handle link renaming [[foo|text]] will show link as "text"
if( "" != $match[3] ) {
$text = $match[3];
@@ -1103,14 +1194,16 @@ class Linker {
*
* @param Revision $rev
* @param bool $local Whether section links should refer to local page
+ * @param $isPublic, show only if all users can see it
* @return string HTML
*/
- function revComment( Revision $rev, $local = false ) {
- if( $rev->userCan( Revision::DELETED_COMMENT ) ) {
+ function revComment( Revision $rev, $local = false, $isPublic = false ) {
+ 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 );
} else {
- $block = " <span class=\"comment\">" .
- wfMsgHtml( 'rev-deleted-comment' ) . "</span>";
+ $block = " <span class=\"comment\">" . wfMsgHtml( 'rev-deleted-comment' ) . "</span>";
}
if( $rev->isDeleted( Revision::DELETED_COMMENT ) ) {
return " <span class=\"history-deleted\">$block</span>";
@@ -1118,6 +1211,18 @@ class Linker {
return $block;
}
+ public function formatRevisionSize( $size ) {
+ if ( $size == 0 ) {
+ $stxt = wfMsgExt( 'historyempty', 'parsemag' );
+ } else {
+ global $wgLang;
+ $stxt = wfMsgExt( 'nbytes', 'parsemag', $wgLang->formatNum( $size ) );
+ $stxt = "($stxt)";
+ }
+ $stxt = htmlspecialchars( $stxt );
+ return "<span class=\"history-size\">$stxt</span>";
+ }
+
/** @todo document */
function tocIndent() {
return "\n<ul>";
@@ -1202,7 +1307,7 @@ class Linker {
$editurl = '&section='.$section;
$url = $this->makeKnownLinkObj(
$nt,
- wfMsg('editsection'),
+ htmlspecialchars(wfMsg('editsection')),
'action=edit'.$editurl,
'', '', '', $hint
);
@@ -1214,13 +1319,13 @@ class Linker {
} elseif( $hook == 'EditSectionLinkForOther' ) {
wfRunHooks( 'EditSectionLinkForOther', array( &$this, $nt, $section, $url, &$result ) );
}
-
+
// For reverse compatibility, add the brackets *after* the hook is run,
// and even add them to hook-provided text.
if( is_null( $result ) ) {
- $result = wfMsg( 'editsection-brackets', $url );
+ $result = wfMsgHtml( 'editsection-brackets', $url );
} else {
- $result = wfMsg( 'editsection-brackets', $result );
+ $result = wfMsgHtml( 'editsection-brackets', $result );
}
return "<span class=\"editsection\">$result</span>";
}
@@ -1282,7 +1387,7 @@ class Linker {
. $this->buildRollbackLink( $rev )
. ']</span>';
}
-
+
/**
* Build a raw rollback link, useful for collections of "tool" links
*
@@ -1299,7 +1404,7 @@ class Linker {
$title,
wfMsgHtml( 'rollbacklink' ),
'action=rollback&from=' . urlencode( $rev->getUserText() ) . $extra
- );
+ );
}
/**
@@ -1337,9 +1442,10 @@ class Linker {
}
$outText .= '</div><ul>';
+ usort( $templates, array( 'Title', 'compare' ) );
foreach ( $templates as $titleObj ) {
$r = $titleObj->getRestrictions( 'edit' );
- if ( in_array( 'sysop', $r ) ) {
+ if ( in_array( 'sysop', $r ) ) {
$protected = wfMsgExt( 'template-protected', array( 'parseinline' ) );
} elseif ( in_array( 'autoconfirmed', $r ) ) {
$protected = wfMsgExt( 'template-semiprotected', array( 'parseinline' ) );
@@ -1353,7 +1459,36 @@ class Linker {
wfProfileOut( __METHOD__ );
return $outText;
}
-
+
+ /**
+ * Returns HTML for the "hidden categories on this page" list.
+ *
+ * @param array $hiddencats Array of hidden categories from Article::getHiddenCategories
+ * or similar
+ * @return string HTML output
+ */
+ public function formatHiddenCategories( $hiddencats) {
+ global $wgUser, $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>';
+
+ 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 .= '</ul>';
+ }
+ wfProfileOut( __METHOD__ );
+ return $outText;
+ }
+
/**
* Format a size in bytes for output, using an appropriate
* unit (B, KB, MB or GB) according to the magnitude in question
@@ -1376,26 +1511,29 @@ class Linker {
* @return string title and accesskey attributes, ready to drop in an
* element (e.g., ' title="This does something [x]" accesskey="x"').
*/
- public function tooltipAndAccesskey($name) {
- $fname="Linker::tooltipAndAccesskey";
- wfProfileIn($fname);
- $out = '';
+ public function tooltipAndAccesskey( $name ) {
+ wfProfileIn( __METHOD__ );
+ $attribs = array();
- $tooltip = wfMsg('tooltip-'.$name);
- if (!wfEmptyMsg('tooltip-'.$name, $tooltip) && $tooltip != '-') {
+ $tooltip = wfMsg( "tooltip-$name" );
+ if( !wfEmptyMsg( "tooltip-$name", $tooltip ) && $tooltip != '-' ) {
// Compatibility: formerly some tooltips had [alt-.] hardcoded
$tooltip = preg_replace( "/ ?\[alt-.\]$/", '', $tooltip );
- $out .= ' title="'.htmlspecialchars($tooltip);
+ $attribs['title'] = $tooltip;
}
- $accesskey = wfMsg('accesskey-'.$name);
- if ($accesskey && $accesskey != '-' && !wfEmptyMsg('accesskey-'.$name, $accesskey)) {
- if ($out) $out .= " [$accesskey]\" accesskey=\"$accesskey\"";
- else $out .= " title=\"[$accesskey]\" accesskey=\"$accesskey\"";
- } elseif ($out) {
- $out .= '"';
+
+ $accesskey = wfMsg( "accesskey-$name" );
+ if( $accesskey && $accesskey != '-' &&
+ !wfEmptyMsg( "accesskey-$name", $accesskey ) ) {
+ if( isset( $attribs['title'] ) ) {
+ $attribs['title'] .= " [$accesskey]";
+ }
+ $attribs['accesskey'] = $accesskey;
}
- wfProfileOut($fname);
- return $out;
+
+ $ret = Xml::expandAttributes( $attribs );
+ wfProfileOut( __METHOD__ );
+ return $ret;
}
/**
@@ -1404,18 +1542,32 @@ class Linker {
* 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.
+ * @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"').
*/
- public function tooltip($name) {
- $out = '';
+ public function tooltip( $name, $options = null ) {
+ wfProfileIn( __METHOD__ );
+
+ $attribs = array();
+
+ $tooltip = wfMsg( "tooltip-$name" );
+ if( !wfEmptyMsg( "tooltip-$name", $tooltip ) && $tooltip != '-' ) {
+ $attribs['title'] = $tooltip;
+ }
- $tooltip = wfMsg('tooltip-'.$name);
- if (!wfEmptyMsg('tooltip-'.$name, $tooltip) && $tooltip != '-') {
- $out = ' title="'.htmlspecialchars($tooltip).'"';
+ if( isset( $attribs['title'] ) && $options == 'withaccess' ) {
+ $accesskey = wfMsg( "accesskey-$name" );
+ if( $accesskey && $accesskey != '-' &&
+ !wfEmptyMsg( "accesskey-$name", $accesskey ) ) {
+ $attribs['title'] .= " [$accesskey]";
+ }
}
- return $out;
+ $ret = Xml::expandAttributes( $attribs );
+ wfProfileOut( __METHOD__ );
+ return $ret;
}
}
diff --git a/includes/LinksUpdate.php b/includes/LinksUpdate.php
index a52414c3..bb192fb9 100644
--- a/includes/LinksUpdate.php
+++ b/includes/LinksUpdate.php
@@ -1,7 +1,7 @@
<?php
/**
* See docs/deferred.txt
- *
+ *
* @todo document (e.g. one-sentence top-level class description).
*/
class LinksUpdate {
@@ -17,6 +17,7 @@ class LinksUpdate {
$mExternals, //!< URLs of external links, array key only
$mCategories, //!< Map of category names to sort keys
$mInterlangs, //!< Map of language codes to titles
+ $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
@@ -46,15 +47,17 @@ class LinksUpdate {
$this->mTitle = $title;
$this->mId = $title->getArticleID();
+ $this->mParserOutput = $parserOutput;
$this->mLinks = $parserOutput->getLinks();
$this->mImages = $parserOutput->getImages();
$this->mTemplates = $parserOutput->getTemplates();
$this->mExternals = $parserOutput->getExternalLinks();
$this->mCategories = $parserOutput->getCategories();
+ $this->mProperties = $parserOutput->getProperties();
# Convert the format of the interlanguage links
- # I didn't want to change it in the ParserOutput, because that array is passed all
- # the way back to the skin, so either a skin API break would be required, or an
+ # I didn't want to change it in the ParserOutput, because that array is passed all
+ # the way back to the skin, so either a skin API break would be required, or an
# inefficient back-conversion.
$ill = $parserOutput->getLanguageLinks();
$this->mInterlangs = array();
@@ -64,7 +67,7 @@ class LinksUpdate {
}
$this->mRecursive = $recursive;
-
+
wfRunHooks( 'LinksUpdateConstructed', array( &$this ) );
}
@@ -73,7 +76,7 @@ class LinksUpdate {
*/
function doUpdate() {
global $wgUseDumbLinkUpdate;
-
+
wfRunHooks( 'LinksUpdate', array( &$this ) );
if ( $wgUseDumbLinkUpdate ) {
$this->doDumbUpdate();
@@ -85,9 +88,8 @@ class LinksUpdate {
}
function doIncrementalUpdate() {
- $fname = 'LinksUpdate::doIncrementalUpdate';
- wfProfileIn( $fname );
-
+ wfProfileIn( __METHOD__ );
+
# Page links
$existing = $this->getExistingLinks();
$this->incrTableUpdate( 'pagelinks', 'pl', $this->getLinkDeletions( $existing ),
@@ -95,11 +97,12 @@ class LinksUpdate {
# Image links
$existing = $this->getExistingImages();
- $this->incrTableUpdate( 'imagelinks', 'il', $this->getImageDeletions( $existing ),
- $this->getImageInsertions( $existing ) );
+
+ $imageDeletes = $this->getImageDeletions( $existing );
+ $this->incrTableUpdate( 'imagelinks', 'il', $imageDeletes, $this->getImageInsertions( $existing ) );
# Invalidate all image description pages which had links added or removed
- $imageUpdates = array_diff_key( $existing, $this->mImages ) + array_diff_key( $this->mImages, $existing );
+ $imageUpdates = $imageDeletes + array_diff_key( $this->mImages, $existing );
$this->invalidateImageDescriptions( $imageUpdates );
# External links
@@ -119,20 +122,35 @@ class LinksUpdate {
# Category links
$existing = $this->getExistingCategories();
- $this->incrTableUpdate( 'categorylinks', 'cl', $this->getCategoryDeletions( $existing ),
- $this->getCategoryInsertions( $existing ) );
+
+ $categoryDeletes = $this->getCategoryDeletions( $existing );
+
+ $this->incrTableUpdate( 'categorylinks', 'cl', $categoryDeletes, $this->getCategoryInsertions( $existing ) );
# Invalidate all categories which were added, deleted or changed (set symmetric difference)
- $categoryUpdates = array_diff_assoc( $existing, $this->mCategories ) + array_diff_assoc( $this->mCategories, $existing );
+ $categoryInserts = array_diff_assoc( $this->mCategories, $existing );
+ $categoryUpdates = $categoryInserts + $categoryDeletes;
$this->invalidateCategories( $categoryUpdates );
+ $this->updateCategoryCounts( $categoryInserts, $categoryDeletes );
+
+ # Page properties
+ $existing = $this->getExistingProperties();
+
+ $propertiesDeletes = $this->getPropertyDeletions( $existing );
+
+ $this->incrTableUpdate( 'page_props', 'pp', $propertiesDeletes, $this->getPropertyInsertions( $existing ) );
+
+ # Invalidate the necessary pages
+ $changed = $propertiesDeletes + array_diff_assoc( $this->mProperties, $existing );
+ $this->invalidateProperties( $changed );
# Refresh links of all pages including this page
# This will be in a separate transaction
if ( $this->mRecursive ) {
$this->queueRecursiveJobs();
}
-
- wfProfileOut( $fname );
+
+ wfProfileOut( __METHOD__ );
}
/**
@@ -141,12 +159,13 @@ class LinksUpdate {
* Also useful where link table corruption needs to be repaired, e.g. in refreshLinks.php
*/
function doDumbUpdate() {
- $fname = 'LinksUpdate::doDumbUpdate';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
# Refresh category pages and image description pages
$existing = $this->getExistingCategories();
- $categoryUpdates = array_diff_assoc( $existing, $this->mCategories ) + array_diff_assoc( $this->mCategories, $existing );
+ $categoryInserts = array_diff_assoc( $this->mCategories, $existing );
+ $categoryDeletes = array_diff_assoc( $existing, $this->mCategories );
+ $categoryUpdates = $categoryInserts + $categoryDeletes;
$existing = $this->getExistingImages();
$imageUpdates = array_diff_key( $existing, $this->mImages ) + array_diff_key( $this->mImages, $existing );
@@ -155,10 +174,13 @@ class LinksUpdate {
$this->dumbTableUpdate( 'categorylinks', $this->getCategoryInsertions(), 'cl_from' );
$this->dumbTableUpdate( 'templatelinks', $this->getTemplateInsertions(), 'tl_from' );
$this->dumbTableUpdate( 'externallinks', $this->getExternalInsertions(), 'el_from' );
- $this->dumbTableUpdate( 'langlinks', $this->getInterlangInsertions(), 'll_from' );
+ $this->dumbTableUpdate( 'langlinks', $this->getInterlangInsertions(),'ll_from' );
+ $this->dumbTableUpdate( 'page_props', $this->getPropertyInsertions(), 'pp_page' );
- # Update the cache of all the category pages and image description pages which were changed
+ # Update the cache of all the category pages and image description
+ # pages which were changed, and fix the category table count
$this->invalidateCategories( $categoryUpdates );
+ $this->updateCategoryCounts( $categoryInserts, $categoryDeletes );
$this->invalidateImageDescriptions( $imageUpdates );
# Refresh links of all pages including this page
@@ -167,18 +189,18 @@ class LinksUpdate {
$this->queueRecursiveJobs();
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
}
function queueRecursiveJobs() {
wfProfileIn( __METHOD__ );
-
+
$batchSize = 100;
$dbr = wfGetDB( DB_SLAVE );
- $res = $dbr->select( array( 'templatelinks', 'page' ),
+ $res = $dbr->select( array( 'templatelinks', 'page' ),
array( 'page_namespace', 'page_title' ),
- array(
- 'page_id=tl_from',
+ array(
+ 'page_id=tl_from',
'tl_namespace' => $this->mTitle->getNamespace(),
'tl_title' => $this->mTitle->getDBkey()
), __METHOD__
@@ -201,7 +223,7 @@ class LinksUpdate {
$dbr->freeResult( $res );
wfProfileOut( __METHOD__ );
}
-
+
/**
* Invalidate the cache of a list of pages from a single namespace
*
@@ -209,12 +231,10 @@ class LinksUpdate {
* @param array $dbkeys
*/
function invalidatePages( $namespace, $dbkeys ) {
- $fname = 'LinksUpdate::invalidatePages';
-
if ( !count( $dbkeys ) ) {
return;
}
-
+
/**
* Determine which pages need to be updated
* This is necessary to prevent the job queue from smashing the DB with
@@ -222,12 +242,12 @@ class LinksUpdate {
*/
$now = $this->mDb->timestamp();
$ids = array();
- $res = $this->mDb->select( 'page', array( 'page_id' ),
- array(
+ $res = $this->mDb->select( 'page', array( 'page_id' ),
+ array(
'page_namespace' => $namespace,
'page_title IN (' . $this->mDb->makeList( $dbkeys ) . ')',
'page_touched < ' . $this->mDb->addQuotes( $now )
- ), $fname
+ ), __METHOD__
);
while ( $row = $this->mDb->fetchObject( $res ) ) {
$ids[] = $row->page_id;
@@ -235,17 +255,17 @@ class LinksUpdate {
if ( !count( $ids ) ) {
return;
}
-
+
/**
* Do the update
- * We still need the page_touched condition, in case the row has changed since
+ * We still need the page_touched condition, in case the row has changed since
* the non-locking select above.
*/
- $this->mDb->update( 'page', array( 'page_touched' => $now ),
- array(
+ $this->mDb->update( 'page', array( 'page_touched' => $now ),
+ array(
'page_id IN (' . $this->mDb->makeList( $ids ) . ')',
'page_touched < ' . $this->mDb->addQuotes( $now )
- ), $fname
+ ), __METHOD__
);
}
@@ -253,18 +273,29 @@ class LinksUpdate {
$this->invalidatePages( NS_CATEGORY, array_keys( $cats ) );
}
+ /**
+ * Update all the appropriate counts in the category table.
+ * @param $added associative array of category name => sort key
+ * @param $deleted associative array of category name => sort key
+ */
+ function updateCategoryCounts( $added, $deleted ) {
+ $a = new Article($this->mTitle);
+ $a->updateCategoryCounts(
+ array_keys( $added ), array_keys( $deleted )
+ );
+ }
+
function invalidateImageDescriptions( $images ) {
$this->invalidatePages( NS_IMAGE, array_keys( $images ) );
}
function dumbTableUpdate( $table, $insertions, $fromField ) {
- $fname = 'LinksUpdate::dumbTableUpdate';
- $this->mDb->delete( $table, array( $fromField => $this->mId ), $fname );
+ $this->mDb->delete( $table, array( $fromField => $this->mId ), __METHOD__ );
if ( count( $insertions ) ) {
- # The link array was constructed without FOR UPDATE, so there may be collisions
- # This may cause minor link table inconsistencies, which is better than
- # crippling the site with lock contention.
- $this->mDb->insert( $table, $insertions, $fname, array( 'IGNORE' ) );
+ # The link array was constructed without FOR UPDATE, so there may
+ # be collisions. This may cause minor link table inconsistencies,
+ # which is better than crippling the site with lock contention.
+ $this->mDb->insert( $table, $insertions, __METHOD__, array( 'IGNORE' ) );
}
}
@@ -285,8 +316,12 @@ class LinksUpdate {
* @private
*/
function incrTableUpdate( $table, $prefix, $deletions, $insertions ) {
- $fname = 'LinksUpdate::incrTableUpdate';
- $where = array( "{$prefix}_from" => $this->mId );
+ if ( $table == 'page_props' ) {
+ $fromField = 'pp_page';
+ } else {
+ $fromField = "{$prefix}_from";
+ }
+ $where = array( $fromField => $this->mId );
if ( $table == 'pagelinks' || $table == 'templatelinks' ) {
$clause = $this->makeWhereFrom2d( $deletions, $prefix );
if ( $clause ) {
@@ -297,6 +332,8 @@ class LinksUpdate {
} else {
if ( $table == 'langlinks' ) {
$toField = 'll_lang';
+ } elseif ( $table == 'page_props' ) {
+ $toField = 'pp_propname';
} else {
$toField = $prefix . '_to';
}
@@ -307,10 +344,10 @@ class LinksUpdate {
}
}
if ( $where ) {
- $this->mDb->delete( $table, $where, $fname );
+ $this->mDb->delete( $table, $where, __METHOD__ );
}
if ( count( $insertions ) ) {
- $this->mDb->insert( $table, $insertions, $fname, 'IGNORE' );
+ $this->mDb->insert( $table, $insertions, __METHOD__, 'IGNORE' );
}
}
@@ -412,7 +449,7 @@ class LinksUpdate {
/**
* Get an array of interlanguage link insertions
- * @param array $existing Array mapping existing language codes to titles
+ * @param array $existing Array mapping existing language codes to titles
* @private
*/
function getInterlangInsertions( $existing = array() ) {
@@ -429,6 +466,23 @@ class LinksUpdate {
}
/**
+ * Get an array of page property insertions
+ */
+ function getPropertyInsertions( $existing = array() ) {
+ $diffs = array_diff_assoc( $this->mProperties, $existing );
+ $arr = array();
+ foreach ( $diffs as $name => $value ) {
+ $arr[] = array(
+ 'pp_page' => $this->mId,
+ 'pp_propname' => $name,
+ 'pp_value' => $value,
+ );
+ }
+ return $arr;
+ }
+
+
+ /**
* Given an array of existing links, returns those links which are not in $this
* and thus should be deleted.
* @private
@@ -471,7 +525,7 @@ class LinksUpdate {
return array_diff_key( $existing, $this->mImages );
}
- /**
+ /**
* Given an array of existing external links, returns those links which are not
* in $this and thus should be deleted.
* @private
@@ -489,7 +543,7 @@ class LinksUpdate {
return array_diff_assoc( $existing, $this->mCategories );
}
- /**
+ /**
* Given an array of existing interlanguage links, returns those links which are not
* in $this and thus should be deleted.
* @private
@@ -499,13 +553,20 @@ class LinksUpdate {
}
/**
+ * Get array of properties which should be deleted.
+ * @private
+ */
+ function getPropertyDeletions( $existing ) {
+ return array_diff_assoc( $existing, $this->mProperties );
+ }
+
+ /**
* Get an array of existing links, as a 2-D array
* @private
*/
function getExistingLinks() {
- $fname = 'LinksUpdate::getExistingLinks';
$res = $this->mDb->select( 'pagelinks', array( 'pl_namespace', 'pl_title' ),
- array( 'pl_from' => $this->mId ), $fname, $this->mOptions );
+ array( 'pl_from' => $this->mId ), __METHOD__, $this->mOptions );
$arr = array();
while ( $row = $this->mDb->fetchObject( $res ) ) {
if ( !isset( $arr[$row->pl_namespace] ) ) {
@@ -522,9 +583,8 @@ class LinksUpdate {
* @private
*/
function getExistingTemplates() {
- $fname = 'LinksUpdate::getExistingTemplates';
$res = $this->mDb->select( 'templatelinks', array( 'tl_namespace', 'tl_title' ),
- array( 'tl_from' => $this->mId ), $fname, $this->mOptions );
+ array( 'tl_from' => $this->mId ), __METHOD__, $this->mOptions );
$arr = array();
while ( $row = $this->mDb->fetchObject( $res ) ) {
if ( !isset( $arr[$row->tl_namespace] ) ) {
@@ -541,9 +601,8 @@ class LinksUpdate {
* @private
*/
function getExistingImages() {
- $fname = 'LinksUpdate::getExistingImages';
$res = $this->mDb->select( 'imagelinks', array( 'il_to' ),
- array( 'il_from' => $this->mId ), $fname, $this->mOptions );
+ array( 'il_from' => $this->mId ), __METHOD__, $this->mOptions );
$arr = array();
while ( $row = $this->mDb->fetchObject( $res ) ) {
$arr[$row->il_to] = 1;
@@ -557,9 +616,8 @@ class LinksUpdate {
* @private
*/
function getExistingExternals() {
- $fname = 'LinksUpdate::getExistingExternals';
$res = $this->mDb->select( 'externallinks', array( 'el_to' ),
- array( 'el_from' => $this->mId ), $fname, $this->mOptions );
+ array( 'el_from' => $this->mId ), __METHOD__, $this->mOptions );
$arr = array();
while ( $row = $this->mDb->fetchObject( $res ) ) {
$arr[$row->el_to] = 1;
@@ -573,9 +631,8 @@ class LinksUpdate {
* @private
*/
function getExistingCategories() {
- $fname = 'LinksUpdate::getExistingCategories';
$res = $this->mDb->select( 'categorylinks', array( 'cl_to', 'cl_sortkey' ),
- array( 'cl_from' => $this->mId ), $fname, $this->mOptions );
+ array( 'cl_from' => $this->mId ), __METHOD__, $this->mOptions );
$arr = array();
while ( $row = $this->mDb->fetchObject( $res ) ) {
$arr[$row->cl_to] = $row->cl_sortkey;
@@ -585,26 +642,60 @@ class LinksUpdate {
}
/**
- * Get an array of existing interlanguage links, with the language code in the key and the
+ * Get an array of existing interlanguage links, with the language code in the key and the
* title in the value.
* @private
*/
function getExistingInterlangs() {
- $fname = 'LinksUpdate::getExistingInterlangs';
- $res = $this->mDb->select( 'langlinks', array( 'll_lang', 'll_title' ),
- array( 'll_from' => $this->mId ), $fname, $this->mOptions );
+ $res = $this->mDb->select( 'langlinks', array( 'll_lang', 'll_title' ),
+ array( 'll_from' => $this->mId ), __METHOD__, $this->mOptions );
$arr = array();
while ( $row = $this->mDb->fetchObject( $res ) ) {
$arr[$row->ll_lang] = $row->ll_title;
}
return $arr;
}
-
+
+ /**
+ * Get an array of existing categories, with the name in the key and sort key in the value.
+ * @private
+ */
+ function getExistingProperties() {
+ $res = $this->mDb->select( 'page_props', array( 'pp_propname', 'pp_value' ),
+ array( 'pp_page' => $this->mId ), __METHOD__, $this->mOptions );
+ $arr = array();
+ while ( $row = $this->mDb->fetchObject( $res ) ) {
+ $arr[$row->pp_propname] = $row->pp_value;
+ }
+ $this->mDb->freeResult( $res );
+ return $arr;
+ }
+
+
/**
* Return the title object of the page being updated
- */
+ */
function getTitle() {
return $this->mTitle;
}
-}
+ /**
+ * Invalidate any necessary link lists related to page property changes
+ */
+ function invalidateProperties( $changed ) {
+ global $wgPagePropLinkInvalidations;
+
+ foreach ( $changed as $name => $value ) {
+ if ( isset( $wgPagePropLinkInvalidations[$name] ) ) {
+ $inv = $wgPagePropLinkInvalidations[$name];
+ if ( !is_array( $inv ) ) {
+ $inv = array( $inv );
+ }
+ foreach ( $inv as $table ) {
+ $update = new HTMLCacheUpdate( $this->mTitle, $table );
+ $update->doUpdate();
+ }
+ }
+ }
+ }
+}
diff --git a/includes/LogEventsList.php b/includes/LogEventsList.php
new file mode 100644
index 00000000..d49f636b
--- /dev/null
+++ b/includes/LogEventsList.php
@@ -0,0 +1,742 @@
+<?php
+# Copyright (C) 2004 Brion Vibber <brion@pobox.com>, 2008 Aaron Schulz
+# 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
+
+class LogEventsList {
+ const NO_ACTION_LINK = 1;
+
+ private $skin;
+ private $out;
+ public $flags;
+
+ function __construct( $skin, $out, $flags = 0 ) {
+ $this->skin = $skin;
+ $this->out = $out;
+ $this->flags = $flags;
+ $this->preCacheMessages();
+ }
+
+ /**
+ * As we use the same small set of messages in various methods and that
+ * they are called often, we call them once and save them in $this->message
+ */
+ 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') );
+ }
+ }
+ }
+
+ /**
+ * Set page title and show header for this log type
+ * @param string $type
+ */
+ public function showHeader( $type ) {
+ if( LogPage::isLogType( $type ) ) {
+ $this->out->setPageTitle( LogPage::logName( $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
+ */
+ public function showOptions( $type='', $user='', $page='', $pattern='', $year='', $month='' ) {
+ global $wgScript, $wgMiserMode;
+ $action = htmlspecialchars( $wgScript );
+ $title = SpecialPage::getTitleFor( 'Log' );
+ $special = htmlspecialchars( $title->getPrefixedDBkey() );
+
+ $this->out->addHTML( "<form action=\"$action\" method=\"get\"><fieldset>" .
+ Xml::element( 'legend', array(), wfMsg( 'log' ) ) .
+ Xml::hidden( 'title', $special ) . "\n" .
+ $this->getTypeMenu( $type ) . "\n" .
+ $this->getUserInput( $user ) . "\n" .
+ $this->getTitleInput( $page ) . "\n" .
+ ( !$wgMiserMode ? ($this->getTitlePattern( $pattern )."\n") : "" ) .
+ "<p>" . $this->getDateMenu( $year, $month ) . "\n" .
+ Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "</p>\n" .
+ "</fieldset></form>" );
+ }
+
+ /**
+ * @return string Formatted HTML
+ * @param string $queryType
+ */
+ private function getTypeMenu( $queryType ) {
+ global $wgLogRestrictions, $wgUser;
+
+ $html = "<select name='type'>\n";
+
+ $validTypes = LogPage::validTypes();
+ $m = array(); // Temporary array
+
+ // First pass to load the log names
+ foreach( $validTypes as $type ) {
+ $text = LogPage::logName( $type );
+ $m[$text] = $type;
+ }
+
+ // Second pass to sort by name
+ ksort($m);
+
+ // Third pass generates sorted XHTML content
+ foreach( $m as $text => $type ) {
+ $selected = ($type == $queryType);
+ // Restricted types
+ if ( isset($wgLogRestrictions[$type]) ) {
+ if ( $wgUser->isAllowed( $wgLogRestrictions[$type] ) ) {
+ $html .= Xml::option( $text, $type, $selected ) . "\n";
+ }
+ } else {
+ $html .= Xml::option( $text, $type, $selected ) . "\n";
+ }
+ }
+
+ $html .= '</select>';
+ return $html;
+ }
+
+ /**
+ * @return string Formatted HTML
+ * @param string $user
+ */
+ private function getUserInput( $user ) {
+ return Xml::inputLabel( wfMsg( 'specialloguserlabel' ), 'user', 'user', 15, $user );
+ }
+
+ /**
+ * @return string Formatted HTML
+ * @param string $title
+ */
+ private function getTitleInput( $title ) {
+ return Xml::inputLabel( wfMsg( 'speciallogtitlelabel' ), 'page', 'page', 20, $title );
+ }
+
+ /**
+ * @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 );
+ }
+
+ /**
+ * @return boolean Checkbox
+ */
+ private function getTitlePattern( $pattern ) {
+ return '<span style="white-space: nowrap">' .
+ Xml::checkLabel( wfMsg( 'log-title-wildcard' ), 'pattern', 'pattern', $pattern ) .
+ '</span>';
+ }
+
+ public function beginLogEventsList() {
+ return "<ul>\n";
+ }
+
+ public function endLogEventsList() {
+ return "</ul>\n";
+ }
+
+ /**
+ * @param Row $row a single row from the result set
+ * @return string Formatted HTML list item
+ * @private
+ */
+ public function logLine( $row ) {
+ global $wgLang, $wgUser, $wgContLang;
+
+ $title = Title::makeTitle( $row->log_namespace, $row->log_title );
+ $time = $wgLang->timeanddate( wfTimestamp(TS_MW, $row->log_timestamp), true );
+ // User links
+ if( self::isDeleted($row,LogPage::DELETED_USER) ) {
+ $userLink = '<span class="history-deleted">' . wfMsgHtml( 'rev-deleted-user' ) . '</span>';
+ } else {
+ $userLink = $this->skin->userLink( $row->log_user, $row->user_name ) .
+ $this->skin->userToolLinks( $row->log_user, $row->user_name, true, 0, $row->user_editcount );
+ }
+ // Comment
+ if( self::isDeleted($row,LogPage::DELETED_COMMENT) ) {
+ $comment = '<span class="history-deleted">' . wfMsgHtml('rev-deleted-comment') . '</span>';
+ } else {
+ $comment = $wgContLang->getDirMark() . $this->skin->commentBlock( $row->log_comment );
+ }
+ // Extract extra parameters
+ $paramArray = LogPage::extractParams( $row->log_params );
+ $revert = $del = '';
+ // Some user can hide log items and have review links
+ if( $wgUser->isAllowed( 'deleterevision' ) ) {
+ $del = $this->showhideLinks( $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' ),
+ $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 ) . ')';
+ }
+ // 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 ) . ')';
+ }
+ } 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.
+ }
+ }
+ // 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 );
+ }
+
+ return "<li>$del$time $userLink $action $comment $revert</li>\n";
+ }
+
+ /**
+ * @param Row $row
+ * @return string
+ */
+ private function showhideLinks( $row ) {
+ $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
+ // If event was hidden from sysops
+ if( !self::userCan( $row, LogPage::DELETED_RESTRICTED ) ) {
+ $del = $this->message['rev-delundel'];
+ } else if( $row->log_type == 'suppress' ) {
+ // No one should be hiding from the oversight log
+ $del = $this->message['rev-delundel'];
+ } else {
+ $target = SpecialPage::getTitleFor( 'Log', $row->log_type );
+ $del = $this->skin->makeKnownLinkObj( $revdel, $this->message['rev-delundel'],
+ 'target=' . $target->getPrefixedUrl() . '&logid='.$row->log_id );
+ // Bolden oversighted content
+ if( self::isDeleted( $row, LogPage::DELETED_RESTRICTED ) )
+ $del = "<strong>$del</strong>";
+ }
+ return "<tt>(<small>$del</small>)</tt>";
+ }
+
+ /**
+ * @param Row $row
+ * @param mixed $type (string/array)
+ * @param string $action
+ * @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 );
+ }
+ }
+
+ /**
+ * 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
+ */
+ public static function userCan( $row, $field ) {
+ if( ( $row->log_deleted & $field ) == $field ) {
+ global $wgUser;
+ $permission = ( $row->log_deleted & LogPage::DELETED_RESTRICTED ) == LogPage::DELETED_RESTRICTED
+ ? 'suppressrevision'
+ : 'deleterevision';
+ wfDebug( "Checking for $permission due to $field match on $row->log_deleted\n" );
+ return $wgUser->isAllowed( $permission );
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * @param Row $row
+ * @param int $field one of DELETED_* bitfield constants
+ * @return bool
+ */
+ public static function isDeleted( $row, $field ) {
+ return ($row->log_deleted & $field) == $field;
+ }
+
+ /**
+ * Quick function to show a short log extract
+ * @param OutputPage $out
+ * @param string $type
+ * @param string $page
+ * @param string $user
+ */
+ public static function showLogExtract( $out, $type='', $page='', $user='' ) {
+ global $wgUser;
+ # Insert list of top 50 or so items
+ $loglist = new LogEventsList( $wgUser->getSkin(), $out, 0 );
+ $pager = new LogPager( $loglist, $type, $user, $page, '' );
+ $logBody = $pager->getBody();
+ if( $logBody ) {
+ $out->addHTML(
+ $loglist->beginLogEventsList() .
+ $logBody .
+ $loglist->endLogEventsList()
+ );
+ } else {
+ $out->addWikiMsg( 'logempty' );
+ }
+ }
+
+ /**
+ * SQL clause to skip forbidden log types for this user
+ * @param Database $db
+ * @returns 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 ) {
+ if( !$wgUser->isAllowed($right) ) {
+ $safetype = $db->strencode( $logtype );
+ $hiddenLogs[] = $safetype;
+ }
+ }
+ if( count($hiddenLogs) == 1 ) {
+ return 'log_type != ' . $db->addQuotes( $hiddenLogs[0] );
+ } elseif( !empty( $hiddenLogs ) ) {
+ return 'log_type NOT IN (' . $db->makeList($hiddenLogs) . ')';
+ }
+ return false;
+ }
+}
+
+/**
+ * @ingroup Pager
+ */
+class LogPager extends ReverseChronologicalPager {
+ private $type = '', $user = '', $title = '', $pattern = '', $year = '', $month = '';
+ 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 ) {
+ parent::__construct();
+ $this->mConds = $conds;
+
+ $this->mLogEventsList = $list;
+
+ $this->limitType( $type );
+ $this->limitUser( $user );
+ $this->limitTitle( $title, $pattern );
+ $this->limitDate( $y, $m );
+ }
+
+ function getDefaultQuery() {
+ $query = parent::getDefaultQuery();
+ $query['type'] = $this->type;
+ $query['month'] = $this->month;
+ $query['year'] = $this->year;
+ return $query;
+ }
+
+ /**
+ * 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
+ */
+ private function limitType( $type ) {
+ global $wgLogRestrictions, $wgUser;
+ // Don't even show header for private logs; don't recognize it...
+ if( isset($wgLogRestrictions[$type]) && !$wgUser->isAllowed($wgLogRestrictions[$type]) ) {
+ $type = '';
+ }
+ // Don't show private logs to unpriviledged users
+ $hideLogs = LogEventsList::getExcludeClause( $this->mDb );
+ if( $hideLogs !== false ) {
+ $this->mConds[] = $hideLogs;
+ }
+ if( empty($type) ) {
+ return false;
+ }
+ $this->type = $type;
+ $this->mConds['log_type'] = $type;
+ }
+
+ /**
+ * Set the log reader to return only entries by the given user.
+ * @param string $name (In)valid user name
+ * @private
+ */
+ function limitUser( $name ) {
+ if( $name == '' ) {
+ return false;
+ }
+ $usertitle = Title::makeTitleSafe( NS_USER, $name );
+ if( is_null($usertitle) ) {
+ return false;
+ }
+ /* Fetch userid at first, if known, provides awesome query plan afterwards */
+ $userid = User::idFromName( $name );
+ if( !$userid ) {
+ /* It should be nicer to abort query at all,
+ but for now it won't pass anywhere behind the optimizer */
+ $this->mConds[] = "NULL";
+ } else {
+ $this->mConds['log_user'] = $userid;
+ $this->user = $usertitle->getText();
+ }
+ }
+
+ /**
+ * 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
+ */
+ function limitTitle( $page, $pattern ) {
+ global $wgMiserMode;
+
+ $title = Title::newFromText( $page );
+ if( strlen($page) == 0 || !$title instanceof Title )
+ return false;
+
+ $this->title = $title->getPrefixedText();
+ $ns = $title->getNamespace();
+ # Using the (log_namespace, log_title, log_timestamp) index with a
+ # range scan (LIKE) on the first two parts, instead of simple equality,
+ # makes it unusable for sorting. Sorted retrieval using another index
+ # would be possible, but then we might have to scan arbitrarily many
+ # nodes of that index. Therefore, we need to avoid this if $wgMiserMode
+ # is on.
+ #
+ # This is not a problem with simple title matches, because then we can
+ # use the page_time index. That should have no more than a few hundred
+ # log entries for even the busiest pages, so it can be safely scanned
+ # in full to satisfy an impossible condition on user or similar.
+ if( $pattern && !$wgMiserMode ) {
+ # use escapeLike to avoid expensive search patterns like 't%st%'
+ $safetitle = $this->mDb->escapeLike( $title->getDBkey() );
+ $this->mConds['log_namespace'] = $ns;
+ $this->mConds[] = "log_title LIKE '$safetitle%'";
+ $this->pattern = $pattern;
+ } else {
+ $this->mConds['log_namespace'] = $ns;
+ $this->mConds['log_title'] = $title->getDBkey();
+ }
+ }
+
+ /**
+ * 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() {
+ $this->mConds[] = 'user_id = log_user';
+ # Don't use the wrong logging index
+ if( $this->title || $this->pattern || $this->user ) {
+ $index = array( 'USE INDEX' => array( 'logging' => array('page_time','user_time') ) );
+ } else if( $this->type ) {
+ $index = array( 'USE INDEX' => array( 'logging' => 'type_time' ) );
+ } else {
+ $index = array( 'USE INDEX' => array( 'logging' => 'times' ) );
+ }
+ return array(
+ 'tables' => array( 'logging', 'user' ),
+ 'fields' => array( 'log_type', 'log_action', 'log_user', 'log_namespace', 'log_title', 'log_params',
+ 'log_comment', 'log_id', 'log_deleted', 'log_timestamp', 'user_name', 'user_editcount' ),
+ 'conds' => $this->mConds,
+ 'options' => $index
+ );
+ }
+
+ function getIndexField() {
+ return 'log_timestamp';
+ }
+
+ function getStartBody() {
+ wfProfileIn( __METHOD__ );
+ # Do a link batch query
+ if( $this->getNumRows() > 0 ) {
+ $lb = new LinkBatch;
+ while( $row = $this->mResult->fetchObject() ) {
+ $lb->add( $row->log_namespace, $row->log_title );
+ $lb->addObj( Title::makeTitleSafe( NS_USER, $row->user_name ) );
+ $lb->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->user_name ) );
+ }
+ $lb->execute();
+ $this->mResult->seek( 0 );
+ }
+ wfProfileOut( __METHOD__ );
+ return '';
+ }
+
+ function formatRow( $row ) {
+ return $this->mLogEventsList->logLine( $row );
+ }
+
+ public function getType() {
+ return $this->type;
+ }
+
+ public function getUser() {
+ return $this->user;
+ }
+
+ public function getPage() {
+ return $this->title;
+ }
+
+ public function getPattern() {
+ return $this->pattern;
+ }
+
+ public function getYear() {
+ return $this->year;
+ }
+
+ public function getMonth() {
+ return $this->month;
+ }
+}
+
+/**
+ * @deprecated
+ * @ingroup SpecialPage
+ */
+class LogReader {
+ var $pager;
+ /**
+ * @param WebRequest $request For internal use use a FauxRequest object to pass arbitrary parameters.
+ */
+ function __construct( $request ) {
+ global $wgUser, $wgOut;
+ # 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' );
+ # Don't let the user get stuck with a certain date
+ $skip = $request->getText( 'offset' ) || $request->getText( 'dir' ) == 'prev';
+ if( $skip ) {
+ $y = '';
+ $m = '';
+ }
+ # 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 );
+ }
+
+ /**
+ * Is there at least one row?
+ * @return bool
+ */
+ public function hasRows() {
+ return isset($this->pager) ? ($this->pager->getNumRows() > 0) : false;
+ }
+}
+
+/**
+ * @deprecated
+ * @ingroup SpecialPage
+ */
+class LogViewer {
+ const NO_ACTION_LINK = 1;
+ /**
+ * @var LogReader $reader
+ */
+ var $reader;
+ /**
+ * @param LogReader &$reader where to get our data from
+ * @param integer $flags Bitwise combination of flags:
+ * LogEventsList::NO_ACTION_LINK Don't show restore/unblock/block links
+ */
+ function __construct( &$reader, $flags = 0 ) {
+ global $wgUser;
+ $this->reader =& $reader;
+ $this->reader->pager->mLogEventsList->flags = $flags;
+ # Aliases for shorter code...
+ $this->pager =& $this->reader->pager;
+ $this->list =& $this->reader->pager->mLogEventsList;
+ }
+
+ /**
+ * Take over the whole output page in $wgOut with the log display.
+ */
+ public function show() {
+ # Set title and add header
+ $this->list->showHeader( $pager->getType() );
+ # Show form options
+ $this->list->showOptions( $this->pager->getType(), $this->pager->getUser(), $this->pager->getPage(),
+ $this->pager->getPattern(), $this->pager->getYear(), $this->pager->getMonth() );
+ # Insert list
+ $logBody = $this->pager->getBody();
+ if( $logBody ) {
+ $wgOut->addHTML(
+ $this->pager->getNavigationBar() .
+ $this->list->beginLogEventsList() .
+ $logBody .
+ $this->list->endLogEventsList() .
+ $this->pager->getNavigationBar()
+ );
+ } else {
+ $wgOut->addWikiMsg( 'logempty' );
+ }
+ }
+
+ /**
+ * 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
+ */
+ public function showList( &$out ) {
+ $logBody = $this->pager->getBody();
+ if( $logBody ) {
+ $out->addHTML(
+ $this->list->beginLogEventsList() .
+ $logBody .
+ $this->list->endLogEventsList()
+ );
+ } else {
+ $out->addWikiMsg( 'logempty' );
+ }
+ }
+}
diff --git a/includes/LogPage.php b/includes/LogPage.php
index 7c89df76..27554308 100644
--- a/includes/LogPage.php
+++ b/includes/LogPage.php
@@ -20,7 +20,7 @@
/**
* Contain log classes
- *
+ * @file
*/
/**
@@ -30,8 +30,12 @@
*
*/
class LogPage {
+ const DELETED_ACTION = 1;
+ const DELETED_COMMENT = 2;
+ const DELETED_USER = 4;
+ const DELETED_RESTRICTED = 8;
/* @access private */
- var $type, $action, $comment, $params, $target;
+ var $type, $action, $comment, $params, $target, $doer;
/* @acess public */
var $updateRecentChanges;
@@ -47,41 +51,40 @@ class LogPage {
$this->updateRecentChanges = $rc;
}
- function saveContent() {
- if( wfReadOnly() ) return false;
-
- global $wgUser;
+ protected function saveContent() {
+ global $wgUser, $wgLogRestrictions;
$fname = 'LogPage::saveContent';
$dbw = wfGetDB( DB_MASTER );
- $uid = $wgUser->getID();
$log_id = $dbw->nextSequenceValue( 'log_log_id_seq' );
$this->timestamp = $now = wfTimestampNow();
$data = array(
+ 'log_id' => $log_id,
'log_type' => $this->type,
'log_action' => $this->action,
'log_timestamp' => $dbw->timestamp( $now ),
- 'log_user' => $uid,
+ 'log_user' => $this->doer->getId(),
'log_namespace' => $this->target->getNamespace(),
'log_title' => $this->target->getDBkey(),
'log_comment' => $this->comment,
'log_params' => $this->params
);
-
- # log_id doesn't exist on Wikimedia servers yet, and it's a tricky
- # schema update to do. Hack it for now to ignore the field on MySQL.
- if ( !is_null( $log_id ) ) {
- $data['log_id'] = $log_id;
- }
$dbw->insert( 'logging', $data, $fname );
+ $newId = !is_null($log_id) ? $log_id : $dbw->insertId();
+ if( !($dbw->affectedRows() > 0) ) {
+ wfDebugLog( "logging", "LogPage::saveContent failed to insert row - Error {$dbw->lastErrno()}: {$dbw->lastError()}" );
+ }
# And update recentchanges
- if ( $this->updateRecentChanges ) {
- $titleObj = SpecialPage::getTitleFor( 'Log', $this->type );
- $rcComment = $this->getRcComment();
- RecentChange::notifyLog( $now, $titleObj, $wgUser, $rcComment, '',
- $this->type, $this->action, $this->target, $this->comment, $this->params );
+ if( $this->updateRecentChanges ) {
+ # Don't add private logs to RC!
+ if( !isset($wgLogRestrictions[$this->type]) || $wgLogRestrictions[$this->type]=='*' ) {
+ $titleObj = SpecialPage::getTitleFor( 'Log', $this->type );
+ $rcComment = $this->getRcComment();
+ RecentChange::notifyLog( $now, $titleObj, $this->doer, $rcComment, '',
+ $this->type, $this->action, $this->target, $this->comment, $this->params, $newId );
+ }
}
return true;
}
@@ -129,24 +132,26 @@ class LogPage {
/**
* @todo handle missing log types
- * @static
+ * @param string $type logtype
+ * @return string Headertext of this logtype
*/
static function logHeader( $type ) {
global $wgLogHeaders;
- return wfMsg( $wgLogHeaders[$type] );
+ return wfMsgExt($wgLogHeaders[$type],array('parseinline'));
}
/**
* @static
+ * @return HTML string
*/
static function actionText( $type, $action, $title = NULL, $skin = NULL, $params = array(), $filterWikilinks=false ) {
global $wgLang, $wgContLang, $wgLogActions;
$key = "$type/$action";
-
+
if( $key == 'patrol/patrol' )
return PatrolLog::makeActionText( $title, $params, $skin );
-
+
if( isset( $wgLogActions[$key] ) ) {
if( is_null( $title ) ) {
$rv=wfMsg( $wgLogActions[$key] );
@@ -155,7 +160,7 @@ class LogPage {
switch( $type ) {
case 'move':
- $titleLink = $skin->makeLinkObj( $title, $title->getPrefixedText(), 'redirect=no' );
+ $titleLink = $skin->makeLinkObj( $title, htmlspecialchars( $title->getPrefixedText() ), 'redirect=no' );
$params[0] = $skin->makeLinkObj( Title::newFromText( $params[0] ), htmlspecialchars( $params[0] ) );
break;
case 'block':
@@ -188,6 +193,11 @@ class LogPage {
if( $key == 'rights/rights' ) {
if ($skin) {
$rightsnone = wfMsg( 'rightsnone' );
+ foreach ( $params as &$param ) {
+ $groupArray = array_map( 'trim', explode( ',', $param ) );
+ $groupArray = array_map( array( 'User', 'getGroupName' ), $groupArray );
+ $param = $wgLang->listToText( $groupArray );
+ }
} else {
$rightsnone = wfMsgForContent( 'rightsnone' );
}
@@ -204,22 +214,28 @@ class LogPage {
}
} else {
array_unshift( $params, $titleLink );
- if ( $key == 'block/block' ) {
+ if ( $key == 'block/block' || $key == 'suppress/block' ) {
if ( $skin ) {
$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] )
+ ? self::formatBlockFlags( $params[2], is_null( $skin ) )
: '';
}
$rv = wfMsgReal( $wgLogActions[$key], $params, true, !$skin );
}
}
} else {
- wfDebug( "LogPage::actionText - unknown action $key\n" );
- $rv = "$action";
+ global $wgLogActionsHandlers;
+ if( isset( $wgLogActionsHandlers[$key] ) ) {
+ $args = func_get_args();
+ $rv = call_user_func_array( $wgLogActionsHandlers[$key], $args );
+ } else {
+ wfDebug( "LogPage::actionText - unknown action $key\n" );
+ $rv = "$action";
+ }
}
if( $filterWikilinks ) {
$rv = str_replace( "[[", "", $rv );
@@ -234,8 +250,9 @@ class LogPage {
* @param object &$target A title object.
* @param string $comment Description associated
* @param array $params Parameters passed later to wfMsg.* functions
+ * @param User $doer The user doing the action
*/
- function addEntry( $action, $target, $comment, $params = array() ) {
+ function addEntry( $action, $target, $comment, $params = array(), $doer = null ) {
if ( !is_array( $params ) ) {
$params = array( $params );
}
@@ -244,6 +261,15 @@ class LogPage {
$this->target = $target;
$this->comment = $comment;
$this->params = LogPage::makeParamBlob( $params );
+
+ if ($doer === null) {
+ global $wgUser;
+ $doer = $wgUser;
+ } elseif (!is_object( $doer ) ) {
+ $doer = User::newFromId( $doer );
+ }
+
+ $this->doer = $doer;
$this->actionText = LogPage::actionText( $this->type, $action, $target, NULL, $params );
@@ -269,41 +295,53 @@ class LogPage {
return explode( "\n", $blob );
}
}
-
+
/**
* Convert a comma-delimited list of block log flags
* into a more readable (and translated) form
*
* @param $flags Flags to format
+ * @param $forContent Whether to localize the message depending of the user
+ * language
* @return string
*/
- public static function formatBlockFlags( $flags ) {
+ public static function formatBlockFlags( $flags, $forContent = false ) {
$flags = explode( ',', trim( $flags ) );
if( count( $flags ) > 0 ) {
for( $i = 0; $i < count( $flags ); $i++ )
- $flags[$i] = self::formatBlockFlag( $flags[$i] );
+ $flags[$i] = self::formatBlockFlag( $flags[$i], $forContent );
return '(' . implode( ', ', $flags ) . ')';
} else {
return '';
}
}
-
+
/**
* Translate a block log flag if possible
*
* @param $flag Flag to translate
+ * @param $forContent Whether to localize the message depending of the user
+ * language
* @return string
*/
- public static function formatBlockFlag( $flag ) {
+ public static function formatBlockFlag( $flag, $forContent = false ) {
static $messages = array();
if( !isset( $messages[$flag] ) ) {
$k = 'block-log-flags-' . $flag;
- $msg = wfMsg( $k );
+ if( $forContent )
+ $msg = wfMsgForContent( $k );
+ else
+ $msg = wfMsg( $k );
$messages[$flag] = htmlspecialchars( wfEmptyMsg( $k, $msg ) ? $flag : $msg );
}
return $messages[$flag];
}
-
}
-
+/**
+ * Aliases for backwards compatibility with 1.6
+ */
+define( 'MW_LOG_DELETED_ACTION', LogPage::DELETED_ACTION );
+define( 'MW_LOG_DELETED_USER', LogPage::DELETED_USER );
+define( 'MW_LOG_DELETED_COMMENT', LogPage::DELETED_COMMENT );
+define( 'MW_LOG_DELETED_RESTRICTED', LogPage::DELETED_RESTRICTED );
diff --git a/includes/MacBinary.php b/includes/MacBinary.php
index da357e52..b5b11e6c 100644
--- a/includes/MacBinary.php
+++ b/includes/MacBinary.php
@@ -22,7 +22,7 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
- * @addtogroup SpecialPage
+ * @ingroup SpecialPage
*/
class MacBinary {
@@ -267,5 +267,3 @@ class MacBinary {
}
}
}
-
-
diff --git a/includes/MagicWord.php b/includes/MagicWord.php
index 18c931c5..3b22cb9b 100644
--- a/includes/MagicWord.php
+++ b/includes/MagicWord.php
@@ -1,7 +1,10 @@
<?php
/**
* File for magic words
- * @addtogroup Parser
+ * See docs/magicword.txt
+ *
+ * @file
+ * @ingroup Parser
*/
/**
@@ -16,10 +19,11 @@
* Please avoid reading the data out of one of these objects and then writing
* special case code. If possible, add another match()-like function here.
*
- * To add magic words in an extension, use the LanguageGetMagic hook. For
+ * To add magic words in an extension, use the LanguageGetMagic hook. For
* magic words which are also Parser variables, add a MagicWordwgVariableIDs
* hook. Use string keys.
*
+ * @ingroup Parser
*/
class MagicWord {
/**#@+
@@ -100,8 +104,9 @@ class MagicWord {
'pagesinnamespace',
'numberofadmins',
'defaultsort',
+ 'pagesincategory',
);
-
+
/* Array of caching hints for ParserCache */
static public $mCacheTTLs = array (
'currentmonth' => 86400,
@@ -140,7 +145,20 @@ class MagicWord {
'numberofadmins' => 3600,
);
+ static public $mDoubleUnderscoreIDs = array(
+ 'notoc',
+ 'nogallery',
+ 'forcetoc',
+ 'toc',
+ 'noeditsection',
+ 'newsectionlink',
+ 'hiddencat',
+ 'staticredirect',
+ );
+
+
static public $mObjects = array();
+ static public $mDoubleUnderscoreArray = null;
/**#@-*/
@@ -188,7 +206,7 @@ class MagicWord {
}
return self::$mVariableIDs;
}
-
+
/* Allow external reads of TTL array */
static function getCacheTTL($id) {
if (array_key_exists($id,self::$mCacheTTLs)) {
@@ -197,8 +215,15 @@ class MagicWord {
return -1;
}
}
-
-
+
+ /** Get a MagicWordArray of double-underscore entities */
+ static function getDoubleUnderscoreArray() {
+ if ( is_null( self::$mDoubleUnderscoreArray ) ) {
+ self::$mDoubleUnderscoreArray = new MagicWordArray( self::$mDoubleUnderscoreIDs );
+ }
+ return self::$mDoubleUnderscoreArray;
+ }
+
# Initialises this object with an ID
function load( $id ) {
global $wgContLang;
@@ -220,13 +245,13 @@ class MagicWord {
# This was used for matching "$1" variables, but different uses of the feature will have
# different restrictions, which should be checked *after* the MagicWord has been matched,
# not here. - IMSoP
-
+
$escSyn = array();
foreach ( $this->mSynonyms as $synonym )
// In case a magic word contains /, like that's going to happen;)
$escSyn[] = preg_quote( $synonym, '/' );
$this->mBaseRegex = implode( '|', $escSyn );
-
+
$case = $this->mCaseSensitive ? '' : 'iu';
$this->mRegex = "/{$this->mBaseRegex}/{$case}";
$this->mRegexStart = "/^(?:{$this->mBaseRegex})/{$case}";
@@ -444,11 +469,13 @@ class MagicWord {
/**
* Class for handling an array of magic words
+ * @ingroup Parser
*/
class MagicWordArray {
var $names = array();
var $hash;
var $baseRegex, $regex;
+ var $matches;
function __construct( $names = array() ) {
$this->names = $names;
@@ -555,6 +582,8 @@ class MagicWordArray {
/**
* Parse a match array from preg_match
+ * Returns array(magic word ID, parameter value)
+ * If there is no parameter value, that element will be false.
*/
function parseMatch( $m ) {
reset( $m );
@@ -579,7 +608,7 @@ class MagicWordArray {
/**
* Match some text, with parameter capture
- * Returns an array with the magic word name in the first element and the
+ * Returns an array with the magic word name in the first element and the
* parameter in the second element.
* Both elements are false if there was no match.
*/
@@ -613,4 +642,25 @@ class MagicWordArray {
}
return false;
}
+
+ /**
+ * Returns an associative array, ID => param value, for all items that match
+ * Removes the matched items from the input string (passed by reference)
+ */
+ public function matchAndRemove( &$text ) {
+ $found = array();
+ $regexes = $this->getRegex();
+ foreach ( $regexes as $regex ) {
+ if ( $regex === '' ) {
+ continue;
+ }
+ preg_match_all( $regex, $text, $matches, PREG_SET_ORDER );
+ foreach ( $matches as $m ) {
+ list( $name, $param ) = $this->parseMatch( $m );
+ $found[$name] = $param;
+ }
+ $text = preg_replace( $regex, '', $text );
+ }
+ return $found;
+ }
}
diff --git a/includes/Math.php b/includes/Math.php
index cfed9554..871e9fc3 100644
--- a/includes/Math.php
+++ b/includes/Math.php
@@ -1,6 +1,8 @@
<?php
/**
* Contain everything related to <math> </math> parsing
+ * @file
+ * @ingroup Parser
*/
/**
@@ -8,8 +10,8 @@
* to rasterized PNG and HTML and MathML approximations. An appropriate
* rendering form is picked and returned.
*
- * by Tomasz Wegrzanowski, with additions by Brion Vibber (2003, 2004)
- *
+ * @author Tomasz Wegrzanowski, with additions by Brion Vibber (2003, 2004)
+ * @ingroup Parser
*/
class MathRenderer {
var $mode = MW_MATH_MODERN;
@@ -257,7 +259,7 @@ class MathRenderer {
$this->html );
}
}
-
+
function _attribs( $tag, $defaults=array(), $overrides=array() ) {
$attribs = Sanitizer::validateTagAttributes( $this->params, $tag );
$attribs = Sanitizer::mergeAttributes( $defaults, $attribs );
@@ -297,4 +299,3 @@ class MathRenderer {
return $math->render();
}
}
-
diff --git a/includes/MediaTransformOutput.php b/includes/MediaTransformOutput.php
index c6cf9ac2..9e94f06b 100644
--- a/includes/MediaTransformOutput.php
+++ b/includes/MediaTransformOutput.php
@@ -1,9 +1,13 @@
<?php
+/**
+ * @file
+ * @ingroup Media
+ */
/**
* Base class for the output of MediaHandler::doTransform() and File::transform().
*
- * @addtogroup Media
+ * @ingroup Media
*/
abstract class MediaTransformOutput {
var $file, $width, $height, $url, $page, $path;
@@ -13,7 +17,7 @@ abstract class MediaTransformOutput {
*/
function getWidth() {
return $this->width;
- }
+ }
/**
* Get the height of the output box
@@ -39,8 +43,8 @@ abstract class MediaTransformOutput {
/**
* Fetch HTML for this transform output
*
- * @param array $options Associative array of options. Boolean options
- * should be indicated with a value of true for true, and false or
+ * @param array $options Associative array of options. Boolean options
+ * should be indicated with a value of true for true, and false or
* absent for false.
*
* alt Alternate text or caption
@@ -49,8 +53,8 @@ abstract class MediaTransformOutput {
* valign vertical-align property, if the output is an inline element
* img-class Class applied to the <img> tag, if there is such a tag
*
- * For images, desc-link and file-link are implemented as a click-through. For
- * sounds and videos, they may be displayed in other ways.
+ * For images, desc-link and file-link are implemented as a click-through. For
+ * sounds and videos, they may be displayed in other ways.
*
* @return string
*/
@@ -74,13 +78,16 @@ abstract class MediaTransformOutput {
}
}
- function getDescLinkAttribs( $alt = false ) {
+ function getDescLinkAttribs( $alt = false, $params = '' ) {
$query = $this->page ? ( 'page=' . urlencode( $this->page ) ) : '';
+ if( $params ) {
+ $query .= $query ? '&'.$params : $params;
+ }
$title = $this->file->getTitle();
if ( strval( $alt ) === '' ) {
$alt = $title->getText();
}
- return array(
+ return array(
'href' => $this->file->getTitle()->getLocalURL( $query ),
'class' => 'image',
'title' => $alt
@@ -92,7 +99,7 @@ abstract class MediaTransformOutput {
/**
* Media transform output for images
*
- * @addtogroup Media
+ * @ingroup Media
*/
class ThumbnailImage extends MediaTransformOutput {
/**
@@ -115,9 +122,9 @@ class ThumbnailImage extends MediaTransformOutput {
/**
* Return HTML <img ... /> tag for the thumbnail, will include
* width and height attributes and a blank alt text (as required).
- *
- * @param array $options Associative array of options. Boolean options
- * should be indicated with a value of true for true, and false or
+ *
+ * @param array $options Associative array of options. Boolean options
+ * should be indicated with a value of true for true, and false or
* absent for false.
*
* alt Alternate text or caption
@@ -125,9 +132,10 @@ class ThumbnailImage extends MediaTransformOutput {
* 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
*
- * For images, desc-link and file-link are implemented as a click-through. For
- * sounds and videos, they may be displayed in other ways.
+ * For images, desc-link and file-link are implemented as a click-through. For
+ * sounds and videos, they may be displayed in other ways.
*
* @return string
* @public
@@ -138,8 +146,9 @@ class ThumbnailImage extends MediaTransformOutput {
}
$alt = empty( $options['alt'] ) ? '' : $options['alt'];
+ $query = empty($options['desc-query']) ? '' : $options['desc-query'];
if ( !empty( $options['desc-link'] ) ) {
- $linkAttribs = $this->getDescLinkAttribs( $alt );
+ $linkAttribs = $this->getDescLinkAttribs( $alt, $query );
} elseif ( !empty( $options['file-link'] ) ) {
$linkAttribs = array( 'href' => $this->file->getURL() );
} else {
@@ -167,7 +176,7 @@ class ThumbnailImage extends MediaTransformOutput {
/**
* Basic media transform error class
*
- * @addtogroup Media
+ * @ingroup Media
*/
class MediaTransformError extends MediaTransformOutput {
var $htmlMsg, $textMsg, $width, $height, $url, $path;
@@ -208,15 +217,13 @@ class MediaTransformError extends MediaTransformOutput {
/**
* Shortcut class for parameter validation errors
*
- * @addtogroup Media
+ * @ingroup Media
*/
class TransformParameterError extends MediaTransformError {
function __construct( $params ) {
- parent::__construct( 'thumbnail_error',
- max( isset( $params['width'] ) ? $params['width'] : 0, 180 ),
- max( isset( $params['height'] ) ? $params['height'] : 0, 180 ),
+ parent::__construct( 'thumbnail_error',
+ max( isset( $params['width'] ) ? $params['width'] : 0, 180 ),
+ max( isset( $params['height'] ) ? $params['height'] : 0, 180 ),
wfMsg( 'thumbnail_invalid_params' ) );
}
}
-
-
diff --git a/includes/MemcachedSessions.php b/includes/MemcachedSessions.php
index 3b248cf0..e3bcea1b 100644
--- a/includes/MemcachedSessions.php
+++ b/includes/MemcachedSessions.php
@@ -6,6 +6,8 @@
* be necessary to change the cookie settings to work across hostnames.
* See: http://www.php.net/manual/en/function.session-set-save-handler.php
*
+ * @file
+ * @ingroup Cache
*/
/**
@@ -68,5 +70,3 @@ function memsess_gc( $maxlifetime ) {
}
session_set_save_handler( 'memsess_open', 'memsess_close', 'memsess_read', 'memsess_write', 'memsess_destroy', 'memsess_gc' );
-
-
diff --git a/includes/MessageCache.php b/includes/MessageCache.php
index ce717fa8..f24d3b4d 100644
--- a/includes/MessageCache.php
+++ b/includes/MessageCache.php
@@ -1,7 +1,7 @@
<?php
/**
- *
- * @addtogroup Cache
+ * @file
+ * @ingroup Cache
*/
/**
@@ -15,38 +15,37 @@ define( 'MSG_CACHE_VERSION', 1 );
/**
* Message cache
* Performs various MediaWiki namespace-related functions
- *
+ * @ingroup Cache
*/
class MessageCache {
- var $mCache, $mUseCache, $mDisable, $mExpiry;
- var $mMemcKey, $mKeys, $mParserOptions, $mParser;
+ // Holds loaded messages that are defined in MediaWiki namespace.
+ var $mCache;
+
+ var $mUseCache, $mDisable, $mExpiry;
+ var $mKeys, $mParserOptions, $mParser;
var $mExtensionMessages = array();
var $mInitialised = false;
- var $mDeferred = true;
- var $mAllMessagesLoaded;
+ var $mAllMessagesLoaded; // Extension messages
- function __construct( &$memCached, $useDB, $expiry, $memcPrefix) {
- wfProfileIn( __METHOD__ );
+ // Variable for tracking which variables are loaded
+ var $mLoadedLanguages = array();
+ function __construct( &$memCached, $useDB, $expiry, /*ignored*/ $memcPrefix ) {
$this->mUseCache = !is_null( $memCached );
$this->mMemc = &$memCached;
$this->mDisable = !$useDB;
$this->mExpiry = $expiry;
$this->mDisableTransform = false;
- $this->mMemcKey = $memcPrefix.':messages';
$this->mKeys = false; # initialised on demand
$this->mInitialised = true;
$this->mParser = null;
-
- # When we first get asked for a message,
- # then we'll fill up the cache. If we
- # can return a cache hit, this saves
- # some extra milliseconds
- $this->mDeferred = true;
-
- wfProfileOut( __METHOD__ );
}
+
+ /**
+ * ParserOptions is lazy initialised.
+ * Access should probably be protected.
+ */
function getParserOptions() {
if ( !$this->mParserOptions ) {
$this->mParserOptions = new ParserOptions;
@@ -55,22 +54,25 @@ class MessageCache {
}
/**
- * Try to load the cache from a local file
+ * Try to load the cache from a local file.
+ * Actual format of the file depends on the $wgLocalMessageCacheSerialized
+ * setting.
+ *
+ * @param $hash String: the hash of contents, to check validity.
+ * @param $code Mixed: Optional language code, see documenation of load().
+ * @return false on failure.
*/
- function loadFromLocal( $hash ) {
+ function loadFromLocal( $hash, $code ) {
global $wgLocalMessageCache, $wgLocalMessageCacheSerialized;
- if ( $wgLocalMessageCache === false ) {
- return;
- }
-
- $filename = "$wgLocalMessageCache/messages-" . wfWikiID();
+ $filename = "$wgLocalMessageCache/messages-" . wfWikiID() . "-$code";
+ # Check file existence
wfSuppressWarnings();
$file = fopen( $filename, 'r' );
wfRestoreWarnings();
if ( !$file ) {
- return;
+ return false; // No cache file
}
if ( $wgLocalMessageCacheSerialized ) {
@@ -82,37 +84,38 @@ class MessageCache {
while ( !feof( $file ) ) {
$serialized .= fread( $file, 100000 );
}
- $this->setCache( unserialize( $serialized ) );
+ fclose( $file );
+ return $this->setCache( unserialize( $serialized ), $code );
+ } else {
+ fclose( $file );
+ return false; // Wrong hash
}
- fclose( $file );
} else {
$localHash=substr(fread($file,40),8);
fclose($file);
if ($hash!=$localHash) {
- return;
+ return false; // Wrong hash
}
- require("$wgLocalMessageCache/messages-" . wfWikiID());
- $this->setCache( $this->mCache);
+ # Require overwrites the member variable or just shadows it?
+ require( $filename );
+ return $this->setCache( $this->mCache, $code );
}
}
/**
- * Save the cache to a local file
+ * Save the cache to a local file.
*/
- function saveToLocal( $serialized, $hash ) {
- global $wgLocalMessageCache, $wgLocalMessageCacheSerialized;
-
- if ( $wgLocalMessageCache === false ) {
- return;
- }
+ function saveToLocal( $serialized, $hash, $code ) {
+ global $wgLocalMessageCache;
- $filename = "$wgLocalMessageCache/messages-" . wfWikiID();
- $oldUmask = umask( 0 );
- wfMkdirParents( $wgLocalMessageCache, 0777 );
- umask( $oldUmask );
+ $filename = "$wgLocalMessageCache/messages-" . wfWikiID() . "-$code";
+ wfMkdirParents( $wgLocalMessageCache, 0777 ); // might fail
+ wfSuppressWarnings();
$file = fopen( $filename, 'w' );
+ wfRestoreWarnings();
+
if ( !$file ) {
wfDebug( "Unable to open local cache file for writing\n" );
return;
@@ -123,32 +126,33 @@ class MessageCache {
@chmod( $filename, 0666 );
}
- function loadFromScript( $hash ) {
- trigger_error( 'Use of ' . __METHOD__ . ' is deprecated', E_USER_NOTICE );
- $this->loadFromLocal( $hash );
- }
-
- function saveToScript($array, $hash) {
+ function saveToScript( $array, $hash, $code ) {
global $wgLocalMessageCache;
- if ( $wgLocalMessageCache === false ) {
+
+ $filename = "$wgLocalMessageCache/messages-" . wfWikiID() . "-$code";
+ $tempFilename = $filename . '.tmp';
+ wfMkdirParents( $wgLocalMessageCache, 0777 ); // might fail
+
+ wfSuppressWarnings();
+ $file = fopen( $tempFilename, 'w');
+ wfRestoreWarnings();
+
+ if ( !$file ) {
+ wfDebug( "Unable to open local cache file for writing\n" );
return;
}
- $filename = "$wgLocalMessageCache/messages-" . wfWikiID();
- $oldUmask = umask( 0 );
- wfMkdirParents( $wgLocalMessageCache, 0777 );
- umask( $oldUmask );
- $file = fopen( $filename.'.tmp', 'w');
fwrite($file,"<?php\n//$hash\n\n \$this->mCache = array(");
-
+
foreach ($array as $key => $message) {
- fwrite($file, "'". $this->escapeForScript($key).
- "' => '" . $this->escapeForScript($message).
- "',\n");
+ $key = $this->escapeForScript($key);
+ $messages = $this->escapeForScript($message);
+ fwrite($file, "'$key' => '$message',\n");
}
+
fwrite($file,");\n?>");
fclose($file);
- rename($filename.'.tmp',$filename);
+ rename($tempFilename, $filename);
}
function escapeForScript($string) {
@@ -160,238 +164,307 @@ class MessageCache {
/**
* Set the cache to $cache, if it is valid. Otherwise set the cache to false.
*/
- function setCache( $cache ) {
+ function setCache( $cache, $code ) {
if ( isset( $cache['VERSION'] ) && $cache['VERSION'] == MSG_CACHE_VERSION ) {
- $this->mCache = $cache;
+ $this->mCache[$code] = $cache;
+ return true;
} else {
- $this->mCache = false;
+ return false;
}
}
/**
- * Loads messages either from memcached or the database, if not disabled
- * On error, quietly switches to a fallback mode
- * Returns false for a reportable error, true otherwise
+ * Loads messages from caches or from database in this order:
+ * (1) local message cache (if $wgLocalMessageCache is enabled)
+ * (2) memcached
+ * (3) from the database.
+ *
+ * When succesfully loading from (2) or (3), all higher level caches are
+ * updated for the newest version.
+ *
+ * Nothing is loaded if member variable mDisabled is true, either manually
+ * set by calling code or if message loading fails (is this possible?).
+ *
+ * Returns true if cache is already populated or it was succesfully populated,
+ * or false if populating empty cache fails. Also returns true if MessageCache
+ * is disabled.
+ *
+ * @param $code String: language to which load messages
*/
- function load() {
- global $wgLocalMessageCache, $wgLocalMessageCacheSerialized;
+ function load( $code = false ) {
+ global $wgLocalMessageCache;
+
+ if ( !$this->mUseCache ) {
+ return true;
+ }
+ if( !is_string( $code ) ) {
+ # This isn't really nice, so at least make a note about it and try to
+ # fall back
+ wfDebug( __METHOD__ . " called without providing a language code\n" );
+ $code = 'en';
+ }
+
+ # Don't do double loading...
+ if ( isset($this->mLoadedLanguages[$code]) ) return true;
+
+ # 8 lines of code just to say (once) that message cache is disabled
if ( $this->mDisable ) {
static $shownDisabled = false;
if ( !$shownDisabled ) {
- wfDebug( "MessageCache::load(): disabled\n" );
+ wfDebug( __METHOD__ . ": disabled\n" );
$shownDisabled = true;
}
return true;
}
- if ( !$this->mUseCache ) {
- $this->mDeferred = false;
- return true;
- }
- $fname = 'MessageCache::load';
- wfProfileIn( $fname );
- $success = true;
+ # Loading code starts
+ wfProfileIn( __METHOD__ );
+ $success = false; # Keep track of success
+ $where = array(); # Debug info, delayed to avoid spamming debug log too much
+ $cacheKey = wfMemcKey( 'messages', $code ); # Key in memc for messages
- $this->mCache = false;
- # Try local cache
+ # (1) local cache
+ # Hash of the contents is stored in memcache, to detect if local cache goes
+ # out of date (due to update in other thread?)
if ( $wgLocalMessageCache !== false ) {
- wfProfileIn( $fname.'-fromlocal' );
- $hash = $this->mMemc->get( "{$this->mMemcKey}-hash" );
+ wfProfileIn( __METHOD__ . '-fromlocal' );
+
+ $hash = $this->mMemc->get( wfMemcKey( 'messages', $code, 'hash' ) );
if ( $hash ) {
- $this->loadFromLocal( $hash );
- if ( $this->mCache ) {
- wfDebug( "MessageCache::load(): got from local cache\n" );
- }
+ $success = $this->loadFromLocal( $hash, $code );
+ if ( $success ) $where[] = 'got from local cache';
}
- wfProfileOut( $fname.'-fromlocal' );
- }
-
- # Try memcached
- if ( !$this->mCache ) {
- wfProfileIn( $fname.'-fromcache' );
- $this->setCache( $this->mMemc->get( $this->mMemcKey ) );
- if ( $this->mCache ) {
- wfDebug( "MessageCache::load(): got from global cache\n" );
- # Save to local cache
- if ( $wgLocalMessageCache !== false ) {
- $serialized = serialize( $this->mCache );
- if ( !$hash ) {
- $hash = md5( $serialized );
- $this->mMemc->set( "{$this->mMemcKey}-hash", $hash, $this->mExpiry );
- }
- if ($wgLocalMessageCacheSerialized) {
- $this->saveToLocal( $serialized,$hash );
- } else {
- $this->saveToScript( $this->mCache, $hash );
- }
- }
+ wfProfileOut( __METHOD__ . '-fromlocal' );
+ }
+
+ # (2) memcache
+ # Fails if nothing in cache, or in the wrong version.
+ if ( !$success ) {
+ wfProfileIn( __METHOD__ . '-fromcache' );
+ $cache = $this->mMemc->get( $cacheKey );
+ $success = $this->setCache( $cache, $code );
+ if ( $success ) {
+ $where[] = 'got from global cache';
+ $this->saveToCaches( $cache, false, $code );
}
- wfProfileOut( $fname.'-fromcache' );
+ wfProfileOut( __METHOD__ . '-fromcache' );
}
- # If there's nothing in memcached, load all the messages from the database
- if ( !$this->mCache ) {
- wfDebug( "MessageCache::load(): cache is empty\n" );
- $this->lock();
- # Other threads don't need to load the messages if another thread is doing it.
- $success = $this->mMemc->add( $this->mMemcKey.'-status', "loading", MSG_LOAD_TIMEOUT );
- if ( $success ) {
- wfProfileIn( $fname.'-load' );
- wfDebug( "MessageCache::load(): loading all messages from DB\n" );
- $this->loadFromDB();
- wfProfileOut( $fname.'-load' );
-
- # Save in memcached
- # Keep trying if it fails, this is kind of important
- wfProfileIn( $fname.'-save' );
- for ($i=0; $i<20 &&
- !$this->mMemc->set( $this->mMemcKey, $this->mCache, $this->mExpiry );
- $i++ ) {
- usleep(mt_rand(500000,1500000));
- }
+ # (3)
+ # Nothing in caches... so we need create one and store it in caches
+ if ( !$success ) {
+ $where[] = 'cache is empty';
+ $where[] = 'loading from database';
- # Save to local cache
- if ( $wgLocalMessageCache !== false ) {
- $serialized = serialize( $this->mCache );
- $hash = md5( $serialized );
- $this->mMemc->set( "{$this->mMemcKey}-hash", $hash, $this->mExpiry );
- if ($wgLocalMessageCacheSerialized) {
- $this->saveToLocal( $serialized,$hash );
- } else {
- $this->saveToScript( $this->mCache, $hash );
- }
- }
+ $this->lock($cacheKey);
- wfProfileOut( $fname.'-save' );
- if ( $i == 20 ) {
- $this->mMemc->set( $this->mMemcKey.'-status', 'error', 60*5 );
- wfDebug( "MemCached set error in MessageCache: restart memcached server!\n" );
- } else {
- $this->mMemc->delete( $this->mMemcKey.'-status' );
- }
+ $cache = $this->loadFromDB( $code );
+ $success = $this->setCache( $cache, $code );
+ if ( $success ) {
+ $this->saveToCaches( $cache, true, $code );
}
- $this->unlock();
+
+ $this->unlock($cacheKey);
}
- if ( !is_array( $this->mCache ) ) {
- wfDebug( "MessageCache::load(): unable to load cache, disabled\n" );
+ if ( !$success ) {
+ # Bad luck... this should not happen
+ $where[] = 'loading FAILED - cache is disabled';
+ $info = implode( ', ', $where );
+ wfDebug( __METHOD__ . ": Loading $code... $info\n" );
$this->mDisable = true;
$this->mCache = false;
+ } else {
+ # All good, just record the success
+ $info = implode( ', ', $where );
+ wfDebug( __METHOD__ . ": Loading $code... $info\n" );
+ $this->mLoadedLanguages[$code] = true;
}
- wfProfileOut( $fname );
- $this->mDeferred = false;
+ wfProfileOut( __METHOD__ );
return $success;
}
/**
- * Loads all or main part of cacheable messages from the database
+ * Loads cacheable messages from the database. Messages bigger than
+ * $wgMaxMsgCacheEntrySize are assigned a special value, and are loaded
+ * on-demand from the database later.
+ *
+ * @param $code Optional language code, see documenation of load().
+ * @return Array: Loaded messages for storing in caches.
*/
- function loadFromDB() {
- global $wgMaxMsgCacheEntrySize;
-
+ function loadFromDB( $code = false ) {
wfProfileIn( __METHOD__ );
+ global $wgMaxMsgCacheEntrySize, $wgContLanguageCode;
$dbr = wfGetDB( DB_SLAVE );
- $this->mCache = array();
+ $cache = array();
+
+ # Common conditions
+ $conds = array(
+ 'page_is_redirect' => 0,
+ 'page_namespace' => NS_MEDIAWIKI,
+ );
+
+ if ( $code ) {
+ # Is this fast enough. Should not matter if the filtering is done in the
+ # database or in code.
+ if ( $code !== $wgContLanguageCode ) {
+ # Messages for particular language
+ $escapedCode = $dbr->escapeLike( $code );
+ $conds[] = "page_title like '%%/$escapedCode'";
+ } else {
+ # Effectively disallows use of '/' character in NS_MEDIAWIKI for uses
+ # other than language code.
+ $conds[] = "page_title not like '%%/%%'";
+ }
+ }
+
+ # Conditions to fetch oversized pages to ignore them
+ $bigConds = $conds;
+ $bigConds[] = 'page_len > ' . intval( $wgMaxMsgCacheEntrySize );
# Load titles for all oversized pages in the MediaWiki namespace
- $res = $dbr->select( 'page', 'page_title',
- array(
- 'page_len > ' . intval( $wgMaxMsgCacheEntrySize ),
- 'page_is_redirect' => 0,
- 'page_namespace' => NS_MEDIAWIKI,
- ),
- __METHOD__ );
+ $res = $dbr->select( 'page', 'page_title', $bigConds, __METHOD__ );
while ( $row = $dbr->fetchObject( $res ) ) {
- $this->mCache[$row->page_title] = '!TOO BIG';
+ $cache[$row->page_title] = '!TOO BIG';
}
$dbr->freeResult( $res );
- # Load text for the remaining pages
+ # Conditions to load the remaining pages with their contents
+ $smallConds = $conds;
+ $smallConds[] = 'page_latest=rev_id';
+ $smallConds[] = 'rev_text_id=old_id';
+ $smallConds[] = 'page_len <= ' . intval( $wgMaxMsgCacheEntrySize );
+
$res = $dbr->select( array( 'page', 'revision', 'text' ),
array( 'page_title', 'old_text', 'old_flags' ),
- array(
- 'page_is_redirect' => 0,
- 'page_namespace' => NS_MEDIAWIKI,
- 'page_latest=rev_id',
- 'rev_text_id=old_id',
- 'page_len <= ' . intval( $wgMaxMsgCacheEntrySize ) ),
- __METHOD__ );
+ $smallConds, __METHOD__ );
for ( $row = $dbr->fetchObject( $res ); $row; $row = $dbr->fetchObject( $res ) ) {
- $this->mCache[$row->page_title] = ' ' . Revision::getRevisionText( $row );
+ $cache[$row->page_title] = ' ' . Revision::getRevisionText( $row );
}
- $this->mCache['VERSION'] = MSG_CACHE_VERSION;
$dbr->freeResult( $res );
+
+ $cache['VERSION'] = MSG_CACHE_VERSION;
wfProfileOut( __METHOD__ );
+ return $cache;
}
/**
- * Not really needed anymore
+ * Updates cache as necessary when message page is changed
+ *
+ * @param $title String: name of the page changed.
+ * @param $text Mixed: new contents of the page.
*/
- function getKeys() {
- global $wgContLang;
- if ( !$this->mKeys ) {
- $this->mKeys = array();
- $allMessages = Language::getMessagesFor( 'en' );
- foreach ( $allMessages as $key => $unused ) {
- $title = $wgContLang->ucfirst( $key );
- array_push( $this->mKeys, $title );
- }
- }
- return $this->mKeys;
- }
-
- function replace( $title, $text ) {
- global $wgLocalMessageCache, $wgLocalMessageCacheSerialized, $parserMemc;
+ public function replace( $title, $text ) {
global $wgMaxMsgCacheEntrySize;
-
wfProfileIn( __METHOD__ );
- $this->lock();
- $this->load();
- if ( is_array( $this->mCache ) ) {
+
+
+ list( , $code ) = $this->figureMessage( $title );
+
+ $cacheKey = wfMemcKey( 'messages', $code );
+ $this->load($code);
+ $this->lock($cacheKey);
+
+ if ( is_array($this->mCache[$code]) ) {
+ $titleKey = wfMemcKey( 'messages', 'individual', $title );
+
if ( $text === false ) {
# Article was deleted
- unset( $this->mCache[$title] );
- $this->mMemc->delete( "$this->mMemcKey:{$title}" );
+ unset( $this->mCache[$code][$title] );
+ $this->mMemc->delete( $titleKey );
+
} elseif ( strlen( $text ) > $wgMaxMsgCacheEntrySize ) {
- $this->mCache[$title] = '!TOO BIG';
- $this->mMemc->set( "$this->mMemcKey:{$title}", ' '.$text, $this->mExpiry );
+ # Check for size
+ $this->mCache[$code][$title] = '!TOO BIG';
+ $this->mMemc->set( $titleKey, ' ' . $text, $this->mExpiry );
+
} else {
- $this->mCache[$title] = ' ' . $text;
- $this->mMemc->delete( "$this->mMemcKey:{$title}" );
+ $this->mCache[$code][$title] = ' ' . $text;
+ $this->mMemc->delete( $titleKey );
}
- $this->mMemc->set( $this->mMemcKey, $this->mCache, $this->mExpiry );
-
- # Save to local cache
- if ( $wgLocalMessageCache !== false ) {
- $serialized = serialize( $this->mCache );
- $hash = md5( $serialized );
- $this->mMemc->set( "{$this->mMemcKey}-hash", $hash, $this->mExpiry );
- if ($wgLocalMessageCacheSerialized) {
- $this->saveToLocal( $serialized,$hash );
- } else {
- $this->saveToScript( $this->mCache, $hash );
- }
+
+ # Update caches
+ $this->saveToCaches( $this->mCache[$code], true, $code );
+ }
+ $this->unlock($cacheKey);
+
+ // Also delete cached sidebar... just in case it is affected
+ global $parserMemc;
+ $sidebarKey = wfMemcKey( 'sidebar', $code );
+ $parserMemc->delete( $sidebarKey );
+
+ wfProfileOut( __METHOD__ );
+ }
+
+ /**
+ * Shortcut to update caches.
+ *
+ * @param $cache Array: cached messages with a version.
+ * @param $cacheKey String: Identifier for the cache.
+ * @param $memc Bool: Wether to update or not memcache.
+ * @param $code String: Language code.
+ * @return False on somekind of error.
+ */
+ protected function saveToCaches( $cache, $memc = true, $code = false ) {
+ wfProfileIn( __METHOD__ );
+ 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 ) {
+ # Save in memcached
+ # Keep trying if it fails, this is kind of important
+
+ for ($i=0; $i<20 &&
+ !$this->mMemc->set( $cacheKey, $cache, $this->mExpiry );
+ $i++ ) {
+ usleep(mt_rand(500000,1500000));
}
}
- $this->unlock();
- $parserMemc->delete(wfMemcKey('sidebar'));
+
+ # Save to local cache
+ if ( $wgLocalMessageCache !== false ) {
+ $serialized = serialize( $cache );
+ $hash = md5( $serialized );
+ $this->mMemc->set( wfMemcKey( 'messages', $code, 'hash' ), $hash, $this->mExpiry );
+ if ($wgLocalMessageCacheSerialized) {
+ $this->saveToLocal( $serialized, $hash, $code );
+ } else {
+ $this->saveToScript( $cache, $hash, $code );
+ }
+ }
+
+ 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__ );
+ return $success;
}
/**
* Returns success
* Represents a write lock on the messages key
*/
- function lock() {
+ function lock($key) {
if ( !$this->mUseCache ) {
return true;
}
- $lockKey = $this->mMemcKey . 'lock';
+ $lockKey = $key . ':lock';
for ($i=0; $i < MSG_WAIT_TIMEOUT && !$this->mMemc->add( $lockKey, 1, MSG_LOCK_TIMEOUT ); $i++ ) {
sleep(1);
}
@@ -399,12 +472,12 @@ class MessageCache {
return $i >= MSG_WAIT_TIMEOUT;
}
- function unlock() {
+ function unlock($key) {
if ( !$this->mUseCache ) {
return;
}
- $lockKey = $this->mMemcKey . 'lock';
+ $lockKey = $key . ':lock';
$this->mMemc->delete( $lockKey );
}
@@ -413,26 +486,47 @@ class MessageCache {
*
* @param string $key The message cache key
* @param bool $useDB Get the message from the DB, false to use only the localisation
- * @param bool $forContent Get the message from the content language rather than the
- * user language
+ * @param string $langcode 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, if it is a
+ * false boolean then use the current users
+ * language (as a fallback for the old parameter
+ * functionality), or if it is a true boolean then
+ * use the wikis content language (also as a
+ * fallback).
* @param bool $isFullKey Specifies whether $key is a two part key "lang/msg".
*/
- function get( $key, $useDB = true, $forContent = true, $isFullKey = false ) {
+ function get( $key, $useDB = true, $langcode = true, $isFullKey = false ) {
global $wgContLanguageCode, $wgContLang, $wgLang;
- if( $forContent ) {
+
+ # 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;
- } else {
+ } 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.' );
+ }
}
+
$langcode = $lang->getCode();
+
# If uninitialised, someone is trying to call this halfway through Setup.php
if( !$this->mInitialised ) {
return '&lt;' . htmlspecialchars($key) . '&gt;';
}
- # If cache initialization was deferred, start it now.
- if( $this->mDeferred && !$this->mDisable && $useDB ) {
- $this->load();
- }
$message = false;
@@ -446,10 +540,11 @@ class MessageCache {
if(!$isFullKey && ($langcode != $wgContLanguageCode) ) {
$title .= '/' . $langcode;
}
- $message = $this->getMsgFromNamespace( $title );
+ $message = $this->getMsgFromNamespace( $title, $langcode );
}
+
# Try the extension array
- if( $message === false && isset( $this->mExtensionMessages[$langcode][$lckey] ) ) {
+ if ( $message === false && isset( $this->mExtensionMessages[$langcode][$lckey] ) ) {
$message = $this->mExtensionMessages[$langcode][$lckey];
}
if ( $message === false && isset( $this->mExtensionMessages['en'][$lckey] ) ) {
@@ -457,11 +552,8 @@ class MessageCache {
}
# Try the array in the language object
- if( $message === false ) {
- #wfDebug( "Trying language object for message $key\n" );
- wfSuppressWarnings();
+ if ( $message === false ) {
$message = $lang->getMessage( $lckey );
- wfRestoreWarnings();
if ( is_null( $message ) ) {
$message = false;
}
@@ -473,14 +565,14 @@ class MessageCache {
$mkey = substr( $lckey, 0, $pos );
$code = substr( $lckey, $pos+1 );
if ( $code ) {
+ # We may get calls for things that are http-urls from sidebar
+ # Let's not load nonexistent languages for those
$validCodes = array_keys( Language::getLanguageNames() );
if ( in_array( $code, $validCodes ) ) {
$message = Language::getMessageFor( $mkey, $code );
if ( is_null( $message ) ) {
$message = false;
}
- } else {
- wfDebug( __METHOD__ . ": Invalid code $code for $mkey/$code, not trying messages array\n" );
}
}
}
@@ -489,7 +581,7 @@ class MessageCache {
if( ($message === false || $message === '-' ) &&
!$this->mDisable && $useDB &&
!$isFullKey && ($langcode != $wgContLanguageCode) ) {
- $message = $this->getMsgFromNamespace( $wgContLang->ucfirst( $lckey ) );
+ $message = $this->getMsgFromNamespace( $wgContLang->ucfirst( $lckey ), $wgContLanguageCode );
}
# Final fallback
@@ -500,21 +592,24 @@ class MessageCache {
}
/**
- * Get a message from the MediaWiki namespace, with caching. The key must
+ * Get a message from the MediaWiki namespace, with caching. The key must
* first be converted to two-part lang/msg form if necessary.
*
- * @param string $title Message cache key with initial uppercase letter
+ * @param $title String: Message cache key with initial uppercase letter.
+ * @param $code String: code denoting the language to try.
*/
- function getMsgFromNamespace( $title ) {
- $message = false;
+ function getMsgFromNamespace( $title, $code ) {
$type = false;
+ $message = false;
- # Try the cache
- if( $this->mUseCache && isset( $this->mCache[$title] ) ) {
- $entry = $this->mCache[$title];
- $type = substr( $entry, 0, 1 );
- if ( $type == ' ' ) {
- return substr( $entry, 1 );
+ if ( $this->mUseCache ) {
+ $this->load( $code );
+ if (isset( $this->mCache[$code][$title] ) ) {
+ $entry = $this->mCache[$code][$title];
+ $type = substr( $entry, 0, 1 );
+ if ( $type == ' ' ) {
+ return substr( $entry, 1 );
+ }
}
}
@@ -525,27 +620,28 @@ class MessageCache {
}
# If there is no cache entry and no placeholder, it doesn't exist
- if ( $type != '!' && $message === false ) {
+ if ( $type !== '!' ) {
return false;
}
- $memcKey = $this->mMemcKey . ':' . $title;
+ $titleKey = wfMemcKey( 'messages', 'individual', $title );
# Try the individual message cache
if ( $this->mUseCache ) {
- $entry = $this->mMemc->get( $memcKey );
+ $entry = $this->mMemc->get( $titleKey );
if ( $entry ) {
$type = substr( $entry, 0, 1 );
- if ( $type == ' ' ) {
+ if ( $type === ' ' ) {
+ # Ok!
$message = substr( $entry, 1 );
- $this->mCache[$title] = $entry;
+ $this->mCache[$code][$title] = $entry;
return $message;
- } elseif ( $entry == '!NONEXISTENT' ) {
+ } elseif ( $entry === '!NONEXISTENT' ) {
return false;
} else {
# Corrupt/obsolete entry, delete it
- $this->mMemc->delete( $memcKey );
+ $this->mMemc->delete( $titleKey );
}
}
@@ -556,45 +652,57 @@ class MessageCache {
if( $revision ) {
$message = $revision->getText();
if ($this->mUseCache) {
- $this->mCache[$title] = ' ' . $message;
- $this->mMemc->set( $memcKey, $message, $this->mExpiry );
+ $this->mCache[$code][$title] = ' ' . $message;
+ $this->mMemc->set( $titleKey, $message, $this->mExpiry );
}
} else {
# Negative caching
# Use some special text instead of false, because false gets converted to '' somewhere
- $this->mMemc->set( $memcKey, '!NONEXISTENT', $this->mExpiry );
- $this->mCache[$title] = false;
+ $this->mMemc->set( $titleKey, '!NONEXISTENT', $this->mExpiry );
+ $this->mCache[$code][$title] = false;
}
-
return $message;
}
function transform( $message, $interface = false ) {
+ // Avoid creating parser if nothing to transfrom
+ if( strpos( $message, '{{' ) === false ) {
+ return $message;
+ }
+
global $wgParser;
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;
+ #wfDebug( __METHOD__ . ": following contents triggered transform: $message\n" );
}
if ( $this->mParser ) {
- if( strpos( $message, '{{' ) !== false ) {
- $popts = $this->getParserOptions();
- $popts->setInterfaceMessage( $interface );
- $message = $this->mParser->transformMsg( $message, $popts );
- }
+ $popts = $this->getParserOptions();
+ $popts->setInterfaceMessage( $interface );
+ $message = $this->mParser->transformMsg( $message, $popts );
}
return $message;
}
function disable() { $this->mDisable = true; }
function enable() { $this->mDisable = false; }
-
+
/** @deprecated */
- function disableTransform() {}
- function enableTransform() {}
- function setTransform( $x ) {}
- function getTransform() { return false; }
+ function disableTransform(){
+ wfDeprecated( __METHOD__ );
+ }
+ function enableTransform() {
+ wfDeprecated( __METHOD__ );
+ }
+ function setTransform( $x ) {
+ wfDeprecated( __METHOD__ );
+ }
+ function getTransform() {
+ wfDeprecated( __METHOD__ );
+ return false;
+ }
/**
* Add a message to the cache
@@ -662,12 +770,14 @@ class MessageCache {
* Clear all stored messages. Mainly used after a mass rebuild.
*/
function clear() {
- global $wgLocalMessageCache;
if( $this->mUseCache ) {
- # Global cache
- $this->mMemc->delete( $this->mMemcKey );
- # Invalidate all local caches
- $this->mMemc->delete( "{$this->mMemcKey}-hash" );
+ $langs = Language::getLanguageNames( false );
+ foreach ( array_keys($langs) as $code ) {
+ # Global cache
+ $this->mMemc->delete( wfMemcKey( 'messages', $code ) );
+ # Invalidate all local caches
+ $this->mMemc->delete( wfMemcKey( 'messages', $code, 'hash' ) );
+ }
}
}
@@ -684,39 +794,37 @@ class MessageCache {
wfRunHooks( 'LoadAllMessages' );
# Some register their messages in $wgExtensionMessagesFiles
foreach ( $wgExtensionMessagesFiles as $name => $file ) {
- if ( $file ) {
- $this->loadMessagesFile( $file );
- $wgExtensionMessagesFiles[$name] = false;
- }
+ wfLoadExtensionMessages( $name );
}
# Still others will respond to neither, they are EVIL. We sometimes need to know!
}
/**
* Load messages from a given file
+ *
+ * @param string $filename Filename of file to load.
+ * @param string $langcode Language to load messages for, or false for
+ * default behvaiour (en, content language and user
+ * language).
*/
- function loadMessagesFile( $filename ) {
+ function loadMessagesFile( $filename, $langcode = false ) {
global $wgLang, $wgContLang;
$messages = $magicWords = false;
require( $filename );
- /*
- * Load only languages that are usually used, and merge all fallbacks,
- * except English.
- */
- $langs = array_unique( array( 'en', $wgContLang->getCode(), $wgLang->getCode() ) );
- foreach( $langs as $code ) {
- $fbcode = $code;
- $mergedMessages = array();
- do {
- if ( isset($messages[$fbcode]) ) {
- $mergedMessages += $messages[$fbcode];
- }
- $fbcode = Language::getFallbackfor( $fbcode );
- } while( $fbcode && $fbcode !== 'en' );
-
- if ( !empty($mergedMessages) )
- $this->addMessages( $mergedMessages, $code );
+ $validCodes = Language::getLanguageNames();
+ if( is_string( $langcode ) && array_key_exists( $langcode, $validCodes ) ) {
+ # Load messages for given language code.
+ $this->processMessagesArray( $messages, $langcode );
+ } elseif( is_string( $langcode ) && !array_key_exists( $langcode, $validCodes ) ) {
+ wfDebug( "Invalid language '$langcode' code passed to MessageCache::loadMessagesFile()" );
+ } else {
+ # Load only languages that are usually used, and merge all
+ # fallbacks, except English.
+ $langs = array_unique( array( 'en', $wgContLang->getCode(), $wgLang->getCode() ) );
+ foreach( $langs as $code ) {
+ $this->processMessagesArray( $messages, $code );
+ }
}
if ( $magicWords !== false ) {
@@ -724,4 +832,36 @@ class MessageCache {
$wgContLang->addMagicWordsByLang( $magicWords );
}
}
+
+ /**
+ * Process an array of messages, loading it into the message cache.
+ *
+ * @param array $messages Messages array.
+ * @param string $langcode Language code to process.
+ */
+ function processMessagesArray( $messages, $langcode ) {
+ $fallbackCode = $langcode;
+ $mergedMessages = array();
+ do {
+ if ( isset($messages[$fallbackCode]) ) {
+ $mergedMessages += $messages[$fallbackCode];
+ }
+ $fallbackCode = Language::getFallbackfor( $fallbackCode );
+ } while( $fallbackCode && $fallbackCode !== 'en' );
+
+ if ( !empty($mergedMessages) )
+ $this->addMessages( $mergedMessages, $langcode );
+ }
+
+ public function figureMessage( $key ) {
+ global $wgContLanguageCode;
+ $pieces = explode('/', $key, 2);
+
+ $key = $pieces[0];
+
+ # Language the user is translating to
+ $langCode = isset($pieces[1]) ? $pieces[1] : $wgContLanguageCode;
+ return array( $key, $langCode );
+ }
+
}
diff --git a/includes/Metadata.php b/includes/Metadata.php
index f5b0b247..a543c73c 100644
--- a/includes/Metadata.php
+++ b/includes/Metadata.php
@@ -21,7 +21,7 @@
*/
/**
- * TODO: Perhaps make this file into a Metadata class, with static methods (declared
+ * 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");
@@ -364,5 +364,3 @@ function getKnownLicenses() {
return $knownLicenses;
}
-
-
diff --git a/includes/MimeMagic.php b/includes/MimeMagic.php
index 2ca5892f..ec4505ab 100644
--- a/includes/MimeMagic.php
+++ b/includes/MimeMagic.php
@@ -9,7 +9,7 @@
* the file mime.types in the includes directory.
*/
define('MM_WELL_KNOWN_MIME_TYPES',<<<END_STRING
-application/ogg ogg ogm
+application/ogg ogg ogm ogv
application/pdf pdf
application/x-javascript js
application/x-shockwave-flash swf
@@ -29,7 +29,7 @@ image/x-portable-pixmap ppm
image/x-xcf xcf
text/plain txt
text/html html htm
-video/ogg ogm ogg
+video/ogg ogm ogg ogv
video/mpeg mpg mpeg
END_STRING
);
@@ -73,7 +73,7 @@ if ($wgLoadFileinfoExtension) {
if(!extension_loaded('fileinfo')) dl('fileinfo.' . PHP_SHLIB_SUFFIX);
}
-/**
+/**
* Implements functions related to mime types such as detection and mapping to
* file extension.
*
@@ -120,7 +120,7 @@ class MimeMagic {
if ( $wgMimeTypeFile == 'includes/mime.types' ) {
$wgMimeTypeFile = "$IP/$wgMimeTypeFile";
}
-
+
if ( $wgMimeTypeFile ) {
if ( is_file( $wgMimeTypeFile ) and is_readable( $wgMimeTypeFile ) ) {
wfDebug( __METHOD__.": loading mime types from $wgMimeTypeFile\n" );
@@ -358,10 +358,10 @@ class MimeMagic {
'bmp', 'tiff', 'tif', 'jpc', 'jp2',
'jpx', 'jb2', 'swc', 'iff', 'wbmp',
'xbm',
-
+
// Formats we recognize magic numbers for
- 'djvu', 'ogg', 'mid', 'pdf', 'wmf', 'xcf',
-
+ 'djvu', 'ogg', 'ogv', 'mid', 'pdf', 'wmf', 'xcf',
+
// XML formats we sure hope we recognize reliably
'svg',
);
@@ -374,7 +374,7 @@ class MimeMagic {
* or misinterpreter by the default mime detection (namely xml based formats like XHTML or SVG).
*
* @param string $file The file to check
- * @param mixed $ext The file extension, or true to extract it from the filename.
+ * @param mixed $ext The file extension, or true to extract it from the filename.
* Set it to false to ignore the extension.
*
* @return string the mime type of $file
@@ -394,7 +394,7 @@ class MimeMagic {
wfDebug(__METHOD__.": final mime type of $file: $mime\n");
return $mime;
}
-
+
function doGuessMimeType( $file, $ext = true ) {
// Read a chunk of the file
wfSuppressWarnings();
@@ -409,20 +409,20 @@ class MimeMagic {
// Multimedia...
'MThd' => 'audio/midi',
'OggS' => 'application/ogg',
-
+
// Image formats...
// Note that WMF may have a bare header, no magic number.
"\x01\x00\x09\x00" => 'application/x-msmetafile', // Possibly prone to false positives?
"\xd7\xcd\xc6\x9a" => 'application/x-msmetafile',
'%PDF' => 'application/pdf',
'gimp xcf' => 'image/x-xcf',
-
+
// Some forbidden fruit...
'MZ' => 'application/octet-stream', // DOS/Windows executable
"\xca\xfe\xba\xbe" => 'application/octet-stream', // Mach-O binary
"\x7fELF" => 'application/octet-stream', // ELF binary
);
-
+
foreach( $headers as $magic => $candidate ) {
if( strncmp( $head, $magic, strlen( $magic ) ) == 0 ) {
wfDebug( __METHOD__ . ": magic header in $file recognized as $candidate\n" );
@@ -451,23 +451,16 @@ class MimeMagic {
wfDebug( __METHOD__ . ": recognized $file as application/x-php\n" );
return "application/x-php";
}
-
+
/*
* look for XML formats (XHTML and SVG)
*/
$xml = new XmlTypeCheck( $file );
if( $xml->wellFormed ) {
- $types = array(
- 'http://www.w3.org/2000/svg:svg' => 'image/svg+xml',
- 'svg' => 'image/svg+xml',
- 'http://www.w3.org/1999/xhtml:html' => 'text/html', // application/xhtml+xml?
- 'html' => 'text/html', // application/xhtml+xml?
- );
- if( isset( $types[$xml->rootElement] ) ) {
- $mime = $types[$xml->rootElement];
- return $mime;
+ global $wgXMLMimeTypes;
+ if( isset( $wgXMLMimeTypes[$xml->rootElement] ) ) {
+ return $wgXMLMimeTypes[$xml->rootElement];
} else {
- /// Fixme -- this would be the place to allow additional XML type checks
return 'application/xml';
}
}
@@ -511,11 +504,11 @@ class MimeMagic {
return $mime;
}
}
-
+
wfSuppressWarnings();
$gis = getimagesize( $file );
wfRestoreWarnings();
-
+
if( $gis && isset( $gis['mime'] ) ) {
$mime = $gis['mime'];
wfDebug( __METHOD__.": getimagesize detected $file as $mime\n" );
@@ -535,13 +528,13 @@ class MimeMagic {
/** Internal mime type detection, please use guessMimeType() for application code instead.
* Detection is done using an external program, if $wgMimeDetectorCommand is set.
* Otherwise, the fileinfo extension and mime_content_type are tried (in this order), if they are available.
- * If the dections fails and $ext is not false, the mime type is guessed from the file extension, using
+ * If the dections fails and $ext is not false, the mime type is guessed from the file extension, using
* guessTypesForExtension.
* If the mime type is still unknown, getimagesize is used to detect the mime type if the file is an image.
* If no mime type can be determined, this function returns "unknown/unknown".
*
* @param string $file The file to check
- * @param mixed $ext The file extension, or true to extract it from the filename.
+ * @param mixed $ext The file extension, or true to extract it from the filename.
* Set it to false to ignore the extension.
*
* @return string the mime type of $file
@@ -714,7 +707,7 @@ class MimeMagic {
if ( !$m ) return MEDIATYPE_UNKNOWN;
$m = explode( ' ', $m );
- } else {
+ } else {
# Normalize mime type
if ( isset( $this->mMimeTypeAliases[$extMime] ) ) {
$extMime = $this->mMimeTypeAliases[$extMime];
@@ -734,5 +727,3 @@ class MimeMagic {
return MEDIATYPE_UNKNOWN;
}
}
-
-
diff --git a/includes/Namespace.php b/includes/Namespace.php
index 57a71282..7c7b7ded 100644
--- a/includes/Namespace.php
+++ b/includes/Namespace.php
@@ -1,6 +1,7 @@
<?php
/**
* Provide things related to namespaces
+ * @file
*/
/**
@@ -42,26 +43,23 @@ if( is_array( $wgExtraNamespaces ) ) {
*
*/
-/*
-WARNING: The statement below may fail on some versions of PHP: see bug 12294
-*/
-
-class Namespace {
+class MWNamespace {
/**
* Can pages in the given namespace be moved?
*
- * @param int $index Namespace index
+ * @param $index Int: namespace index
* @return bool
*/
public static function isMovable( $index ) {
- return !( $index < NS_MAIN || $index == NS_IMAGE || $index == NS_CATEGORY );
+ global $wgAllowImageMoving;
+ return !( $index < NS_MAIN || ($index == NS_IMAGE && !$wgAllowImageMoving) || $index == NS_CATEGORY );
}
/**
* Is the given namespace is a subject (non-talk) namespace?
*
- * @param int $index Namespace index
+ * @param $index Int: namespace index
* @return bool
*/
public static function isMain( $index ) {
@@ -71,7 +69,7 @@ class Namespace {
/**
* Is the given namespace a talk namespace?
*
- * @param int $index Namespace index
+ * @param $index Int: namespace index
* @return bool
*/
public static function isTalk( $index ) {
@@ -82,7 +80,7 @@ class Namespace {
/**
* Get the talk namespace index for a given namespace
*
- * @param int $index Namespace index
+ * @param $index Int: namespace index
* @return int
*/
public static function getTalk( $index ) {
@@ -94,7 +92,7 @@ class Namespace {
/**
* Get the subject namespace index for a given namespace
*
- * @param int $index Namespace index
+ * @param $index Int: Namespace index
* @return int
*/
public static function getSubject( $index ) {
@@ -106,7 +104,7 @@ class Namespace {
/**
* Returns the canonical (English Wikipedia) name for a given index
*
- * @param int $index Namespace index
+ * @param $index Int: namespace index
* @return string
*/
public static function getCanonicalName( $index ) {
@@ -118,7 +116,7 @@ class Namespace {
* Returns the index for a given canonical name, or NULL
* The input *must* be converted to lower case first
*
- * @param string $name Namespace name
+ * @param $name String: namespace name
* @return int
*/
public static function getCanonicalIndex( $name ) {
@@ -136,37 +134,48 @@ class Namespace {
return NULL;
}
}
-
+
/**
* Can this namespace ever have a talk namespace?
*
- * @param $index Namespace index
+ * @param $index Int: namespace index
* @return bool
*/
public static function canTalk( $index ) {
return $index >= NS_MAIN;
}
-
+
/**
- * Does this namespace contain content, for the purposes
- * of calculating statistics, etc?
+ * Does this namespace contain content, for the purposes of calculating
+ * statistics, etc?
*
- * @param $index Index to check
+ * @param $index Int: index to check
* @return bool
*/
public static function isContent( $index ) {
global $wgContentNamespaces;
return $index == NS_MAIN || in_array( $index, $wgContentNamespaces );
}
-
+
/**
* Can pages in a namespace be watched?
*
- * @param int $index
+ * @param $index Int
* @return bool
*/
public static function isWatchable( $index ) {
return $index >= NS_MAIN;
}
-
-} \ No newline at end of file
+
+ /**
+ * Does the namespace allow subpages?
+ *
+ * @param $index int Index to check
+ * @return bool
+ */
+ public static function hasSubpages( $index ) {
+ global $wgNamespacesWithSubpages;
+ return !empty( $wgNamespacesWithSubpages[$index] );
+ }
+
+}
diff --git a/includes/NamespaceCompat.php b/includes/NamespaceCompat.php
new file mode 100644
index 00000000..15c76478
--- /dev/null
+++ b/includes/NamespaceCompat.php
@@ -0,0 +1,9 @@
+<?php
+
+/**
+ * For compatibility with extensions...
+ * Will still die on PHP 5.3, of course. :P
+ */
+class Namespace extends MWNamespace {
+ // ..
+}
diff --git a/includes/ObjectCache.php b/includes/ObjectCache.php
index 7d9caf8a..01b61dfb 100644
--- a/includes/ObjectCache.php
+++ b/includes/ObjectCache.php
@@ -1,6 +1,7 @@
<?php
/**
- * @addtogroup Cache
+ * @file
+ * @ingroup Cache
*/
/**
@@ -8,7 +9,7 @@
* It acts as a memcached server with no RAM, that is, all objects are
* cleared the moment they are set. All set operations succeed and all
* get operations return null.
- * @addtogroup Cache
+ * @ingroup Cache
*/
class FakeMemCachedClient {
function add ($key, $val, $exp = 0) { return true; }
@@ -53,7 +54,7 @@ function &wfGetCache( $inputType ) {
if (!class_exists("MemcachedClientforWiki")) {
class MemCachedClientforWiki extends memcached {
function _debugprint( $text ) {
- wfDebug( "memcached: $text\n" );
+ wfDebug( "memcached: $text" );
}
}
}
@@ -87,7 +88,7 @@ function &wfGetCache( $inputType ) {
}
$cache =& $wgCaches[CACHE_DBA];
}
-
+
if ( $type == CACHE_DB || ( $inputType == CACHE_ANYTHING && $cache === false ) ) {
if ( !array_key_exists( CACHE_DB, $wgCaches ) ) {
$wgCaches[CACHE_DB] = new MediaWikiBagOStuff('objectcache');
@@ -122,5 +123,3 @@ function &wfGetParserCacheStorage() {
$ret =& wfGetCache( $wgParserCacheType );
return $ret;
}
-
-
diff --git a/includes/OutputHandler.php b/includes/OutputHandler.php
index 107553fc..2b3e9fae 100644
--- a/includes/OutputHandler.php
+++ b/includes/OutputHandler.php
@@ -48,7 +48,7 @@ function wfRequestExtension() {
// Can't get the path from the server? :(
return '';
}
-
+
$period = strrpos( $path, '.' );
if( $period !== false ) {
return strtolower( substr( $path, $period ) );
@@ -64,7 +64,7 @@ function wfGzipHandler( $s ) {
if( !function_exists( 'gzencode' ) || headers_sent() ) {
return $s;
}
-
+
$ext = wfRequestExtension();
if( $ext == '.gz' || $ext == '.tgz' ) {
// Don't do gzip compression if the URL path ends in .gz or .tgz
@@ -73,7 +73,7 @@ function wfGzipHandler( $s ) {
// Bad Safari! Bad!
return $s;
}
-
+
if( isset( $_SERVER['HTTP_ACCEPT_ENCODING'] ) ) {
$tokens = preg_split( '/[,; ]/', $_SERVER['HTTP_ACCEPT_ENCODING'] );
if ( in_array( 'gzip', $tokens ) ) {
@@ -81,7 +81,7 @@ function wfGzipHandler( $s ) {
$s = gzencode( $s, 3 );
}
}
-
+
// Set vary header if it hasn't been set already
$headers = headers_list();
$foundVary = false;
@@ -102,7 +102,12 @@ function wfGzipHandler( $s ) {
* Mangle flash policy tags which open up the site to XSS attacks.
*/
function wfMangleFlashPolicy( $s ) {
- return preg_replace( '/\<\s*cross-domain-policy\s*\>/i', '<NOT-cross-domain-policy>', $s );
+ # Avoid weird excessive memory usage in PCRE on big articles
+ if ( preg_match( '/\<\s*cross-domain-policy\s*\>/i', $s ) ) {
+ return preg_replace( '/\<\s*cross-domain-policy\s*\>/i', '<NOT-cross-domain-policy>', $s );
+ } else {
+ return $s;
+ }
}
/**
@@ -170,4 +175,3 @@ EOT;
$out .= '</ol></body></html>';
return $out;
}
-
diff --git a/includes/OutputPage.php b/includes/OutputPage.php
index 1fddeb7d..8226cb2f 100644
--- a/includes/OutputPage.php
+++ b/includes/OutputPage.php
@@ -1,8 +1,6 @@
<?php
if ( ! defined( 'MEDIAWIKI' ) )
die( 1 );
-/**
- */
/**
* @todo document
@@ -26,7 +24,7 @@ class OutputPage {
var $mFeedLinksAppendQuery = false;
var $mEnableClientCache = true;
var $mArticleBodyOnly = false;
-
+
var $mNewSectionLink = false;
var $mNoGallery = false;
var $mPageTitleActionText = '';
@@ -59,13 +57,13 @@ class OutputPage {
$this->mNewSectionLink = false;
$this->mTemplateIds = array();
}
-
+
public function redirect( $url, $responsecode = '302' ) {
# Strip newlines as a paranoia check for header injection in PHP<5.1.2
$this->mRedirect = str_replace( "\n", '', $url );
$this->mRedirectCode = $responsecode;
}
-
+
public function getRedirect() {
return $this->mRedirect;
}
@@ -87,10 +85,26 @@ class OutputPage {
$this->addLink(
array(
'rel' => 'stylesheet',
- 'href' => $wgStylePath . '/' . $style . '?' . $wgStyleVersion ) );
+ 'href' => $wgStylePath . '/' . $style . '?' . $wgStyleVersion,
+ 'type' => 'text/css' ) );
}
/**
+ * Add a JavaScript file out of skins/common, or a given relative path.
+ * @param string $file filename in skins/common or complete on-server path (/foo/bar.js)
+ */
+ function addScriptFile( $file ) {
+ global $wgStylePath, $wgStyleVersion, $wgJsMimeType;
+ if( substr( $file, 0, 1 ) == '/' ) {
+ $path = $file;
+ } else {
+ $path = "{$wgStylePath}/common/{$file}";
+ }
+ $encPath = htmlspecialchars( $path );
+ $this->addScript( "<script type=\"{$wgJsMimeType}\" src=\"$path?$wgStyleVersion\"></script>\n" );
+ }
+
+ /**
* Add a self-contained script tag with the given contents
* @param string $script JavaScript text, no <script> tags
*/
@@ -99,8 +113,8 @@ class OutputPage {
$this->mScripts .= "<script type=\"$wgJsMimeType\">/*<![CDATA[*/\n$script\n/*]]>*/</script>";
}
- function getScript() {
- return $this->mScripts . $this->getHeadItems();
+ function getScript() {
+ return $this->mScripts . $this->getHeadItems();
}
function getHeadItems() {
@@ -145,18 +159,17 @@ class OutputPage {
*/
function checkLastModified ( $timestamp ) {
global $wgCachePages, $wgCacheEpoch, $wgUser, $wgRequest;
- $fname = 'OutputPage::checkLastModified';
if ( !$timestamp || $timestamp == '19700101000000' ) {
- wfDebug( "$fname: CACHE DISABLED, NO TIMESTAMP\n" );
+ wfDebug( __METHOD__ . ": CACHE DISABLED, NO TIMESTAMP\n" );
return;
}
if( !$wgCachePages ) {
- wfDebug( "$fname: CACHE DISABLED\n", false );
+ wfDebug( __METHOD__ . ": CACHE DISABLED\n", false );
return;
}
if( $wgUser->getOption( 'nocache' ) ) {
- wfDebug( "$fname: USER DISABLED CACHE\n", false );
+ wfDebug( __METHOD__ . ": USER DISABLED CACHE\n", false );
return;
}
@@ -168,34 +181,34 @@ class OutputPage {
# Wed, 20 Aug 2003 06:51:19 GMT; length=5202
# this breaks strtotime().
$modsince = preg_replace( '/;.*$/', '', $_SERVER["HTTP_IF_MODIFIED_SINCE"] );
-
+
wfSuppressWarnings(); // E_STRICT system time bitching
$modsinceTime = strtotime( $modsince );
wfRestoreWarnings();
-
+
$ismodsince = wfTimestamp( TS_MW, $modsinceTime ? $modsinceTime : 1 );
- wfDebug( "$fname: -- client send If-Modified-Since: " . $modsince . "\n", false );
- wfDebug( "$fname: -- we might send Last-Modified : $lastmod\n", false );
+ 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( "$fname: CACHED client: $ismodsince ; user: $wgUser->mTouched ; page: $timestamp ; site $wgCacheEpoch\n", false );
+ wfDebug( __METHOD__ . ": CACHED client: $ismodsince ; user: $wgUser->mTouched ; page: $timestamp ; site $wgCacheEpoch\n", false );
$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;
} else {
- wfDebug( "$fname: READY client: $ismodsince ; user: $wgUser->mTouched ; page: $timestamp ; site $wgCacheEpoch\n", false );
+ wfDebug( __METHOD__ . ": READY client: $ismodsince ; user: $wgUser->mTouched ; page: $timestamp ; site $wgCacheEpoch\n", false );
$this->mLastModified = $lastmod;
}
} else {
- wfDebug( "$fname: client did not send If-Modified-Since header\n", false );
+ wfDebug( __METHOD__ . ": client did not send If-Modified-Since header\n", false );
$this->mLastModified = $lastmod;
}
}
@@ -228,6 +241,7 @@ class OutputPage {
public function getHTMLTitle() { return $this->mHTMLtitle; }
public function getPageTitle() { return $this->mPagetitle; }
public function setSubtitle( $str ) { $this->mSubtitle = /*$this->parse(*/$str/*)*/; } // @bug 2514
+ public function appendSubtitle( $str ) { $this->mSubtitle .= /*$this->parse(*/$str/*)*/; } // @bug 2514
public function getSubtitle() { return $this->mSubtitle; }
public function isArticle() { return $this->mIsarticle; }
public function setPrintable() { $this->mPrintable = true; }
@@ -270,23 +284,49 @@ class OutputPage {
/**
* Add an array of categories, with names in the keys
*/
- public function addCategoryLinks($categories) {
+ public function addCategoryLinks( $categories ) {
global $wgUser, $wgContLang;
- if ( !is_array( $categories ) ) {
+ if ( !is_array( $categories ) || count( $categories ) == 0 ) {
return;
}
- # Add the links to the link cache in a batch
+
+ # Add the links to a LinkBatch
$arr = array( NS_CATEGORY => $categories );
$lb = new LinkBatch;
$lb->setArray( $arr );
- $lb->execute();
- $sk = $wgUser->getSkin();
- foreach ( $categories as $category => $unused ) {
- $title = Title::makeTitleSafe( NS_CATEGORY, $category );
- $text = $wgContLang->convertHtml( $title->getText() );
- $this->mCategoryLinks[] = $sk->makeLinkObj( $title, $text );
+ # Fetch existence plus the hiddencat property
+ $dbr = wfGetDB( DB_SLAVE );
+ $pageTable = $dbr->tableName( 'page' );
+ $where = $lb->constructSet( 'page', $dbr );
+ $propsTable = $dbr->tableName( 'page_props' );
+ $sql = "SELECT page_id, page_namespace, page_title, page_len, page_is_redirect, pp_value
+ FROM $pageTable LEFT JOIN $propsTable ON pp_propname='hiddencat' AND pp_page=page_id WHERE $where";
+ $res = $dbr->query( $sql, __METHOD__ );
+
+ # Add the results to the link cache
+ $lb->addResultToCache( LinkCache::singleton(), $res );
+
+ # Set all the values to 'normal'. This can be done with array_fill_keys in PHP 5.2.0+
+ $categories = array_combine( array_keys( $categories ),
+ array_fill( 0, count( $categories ), 'normal' ) );
+
+ # Mark hidden categories
+ foreach ( $res as $row ) {
+ if ( isset( $row->pp_value ) ) {
+ $categories[$row->page_title] = 'hidden';
+ }
+ }
+
+ # Add the remaining categories to the skin
+ if ( wfRunHooks( 'OutputPageMakeCategoryLinks', array( &$this, $categories, &$this->mCategoryLinks ) ) ) {
+ $sk = $wgUser->getSkin();
+ foreach ( $categories as $category => $type ) {
+ $title = Title::makeTitleSafe( NS_CATEGORY, $category );
+ $text = $wgContLang->convertHtml( $title->getText() );
+ $this->mCategoryLinks[$type][] = $sk->makeLinkObj( $title, $text );
+ }
}
}
@@ -308,6 +348,7 @@ class OutputPage {
/* @deprecated */
public function setParserOptions( $options ) {
+ wfDeprecated( __METHOD__ );
return $this->parserOptions( $options );
}
@@ -353,22 +394,21 @@ class OutputPage {
public function addWikiTextTitle($text, &$title, $linestart, $tidy = false) {
global $wgParser;
- $fname = 'OutputPage:addWikiTextTitle';
- wfProfileIn($fname);
+ wfProfileIn( __METHOD__ );
- wfIncrStats('pcache_not_possible');
+ wfIncrStats( 'pcache_not_possible' );
$popts = $this->parserOptions();
- $oldTidy = $popts->setTidy($tidy);
+ $oldTidy = $popts->setTidy( $tidy );
$parserOutput = $wgParser->parse( $text, $title, $popts,
$linestart, true, $this->mRevisionId );
-
+
$popts->setTidy( $oldTidy );
$this->addParserOutput( $parserOutput );
- wfProfileOut($fname);
+ wfProfileOut( __METHOD__ );
}
/**
@@ -387,13 +427,13 @@ class OutputPage {
$this->mNoGallery = $parserOutput->getNoGallery();
$this->mHeadItems = array_merge( $this->mHeadItems, (array)$parserOutput->mHeadItems );
// Versioning...
- $this->mTemplateIds += (array)$parserOutput->mTemplateIds;
-
- # Display title
+ $this->mTemplateIds = wfArrayMerge( $this->mTemplateIds, (array)$parserOutput->mTemplateIds );
+
+ // Display title
if( ( $dt = $parserOutput->getDisplayTitle() ) !== false )
$this->setPageTitle( $dt );
- # Hooks registered in the object
+ // Hooks registered in the object
global $wgParserOutputHooks;
foreach ( $parserOutput->getOutputHooks() as $hookInfo ) {
list( $hookName, $data ) = $hookInfo;
@@ -428,13 +468,15 @@ class OutputPage {
public function addPrimaryWikiText( $text, $article, $cache = true ) {
global $wgParser, $wgUser;
+ wfDeprecated( __METHOD__ );
+
$popts = $this->parserOptions();
$popts->setTidy(true);
$parserOutput = $wgParser->parse( $text, $article->mTitle,
$popts, true, true, $this->mRevisionId );
$popts->setTidy(false);
if ( $cache && $article && $parserOutput->getCacheTime() != -1 ) {
- $parserCache =& ParserCache::singleton();
+ $parserCache = ParserCache::singleton();
$parserCache->save( $parserOutput, $article, $wgUser );
}
@@ -446,6 +488,7 @@ class OutputPage {
*/
public function addSecondaryWikiText( $text, $linestart = true ) {
global $wgTitle;
+ wfDeprecated( __METHOD__ );
$this->addWikiTextTitleTidy($text, $wgTitle, $linestart);
}
@@ -494,7 +537,7 @@ class OutputPage {
* @return bool True if successful, else false.
*/
public function tryParserCache( &$article, $user ) {
- $parserCache =& ParserCache::singleton();
+ $parserCache = ParserCache::singleton();
$parserOutput = $parserCache->get( $article, $user );
if ( $parserOutput !== false ) {
$this->addParserOutput( $parserOutput );
@@ -519,27 +562,70 @@ class OutputPage {
return wfSetVar( $this->mEnableClientCache, $state );
}
- function uncacheableBecauseRequestvars() {
+ function getCacheVaryCookies() {
+ global $wgCookiePrefix, $wgCacheVaryCookies;
+ static $cookies;
+ if ( $cookies === null ) {
+ $cookies = array_merge(
+ array(
+ "{$wgCookiePrefix}Token",
+ "{$wgCookiePrefix}LoggedOut",
+ session_name()
+ ),
+ $wgCacheVaryCookies
+ );
+ wfRunHooks('GetCacheVaryCookies', array( $this, &$cookies ) );
+ }
+ return $cookies;
+ }
+
+ function uncacheableBecauseRequestVars() {
global $wgRequest;
return $wgRequest->getText('useskin', false) === false
&& $wgRequest->getText('uselang', false) === false;
}
+ /**
+ * Check if the request has a cache-varying cookie header
+ * If it does, it's very important that we don't allow public caching
+ */
+ function haveCacheVaryCookies() {
+ global $wgRequest, $wgCookiePrefix;
+ $cookieHeader = $wgRequest->getHeader( 'cookie' );
+ if ( $cookieHeader === false ) {
+ return false;
+ }
+ $cvCookies = $this->getCacheVaryCookies();
+ foreach ( $cvCookies as $cookieName ) {
+ # Check for a simple string match, like the way squid does it
+ if ( strpos( $cookieHeader, $cookieName ) ) {
+ wfDebug( __METHOD__.": found $cookieName\n" );
+ return true;
+ }
+ }
+ wfDebug( __METHOD__.": no cache-varying cookies found\n" );
+ return false;
+ }
+
/** Get a complete X-Vary-Options header */
public function getXVO() {
global $wgCookiePrefix;
- return 'X-Vary-Options: ' .
- # User ID cookie
- "Cookie;string-contains={$wgCookiePrefix}UserID;" .
- # Session cookie
- 'string-contains=' . session_name() . ',' .
- # Encoding checks for gzip only
- 'Accept-Encoding;list-contains=gzip';
+ $cvCookies = $this->getCacheVaryCookies();
+ $xvo = 'X-Vary-Options: Accept-Encoding;list-contains=gzip,Cookie;';
+ $first = true;
+ foreach ( $cvCookies as $cookieName ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $xvo .= ';';
+ }
+ $xvo .= 'string-contains=' . $cookieName;
+ }
+ return $xvo;
}
public function sendCacheControl() {
global $wgUseSquid, $wgUseESI, $wgUseETag, $wgSquidMaxage, $wgRequest;
- $fname = 'OutputPage::sendCacheControl';
$response = $wgRequest->response();
if ($wgUseETag && $this->mETag)
@@ -552,15 +638,15 @@ class OutputPage {
# Add an X-Vary-Options header for Squid with Wikimedia patches
$response->header( $this->getXVO() );
- if( !$this->uncacheableBecauseRequestvars() && $this->mEnableClientCache ) {
+ if( !$this->uncacheableBecauseRequestVars() && $this->mEnableClientCache ) {
if( $wgUseSquid && session_id() == '' &&
- ! $this->isPrintable() && $this->mSquidMaxage != 0 )
+ ! $this->isPrintable() && $this->mSquidMaxage != 0 && !$this->haveCacheVaryCookies() )
{
if ( $wgUseESI ) {
# We'll purge the proxy cache explicitly, but require end user agents
# to revalidate against the proxy on each visit.
# Surrogate-Control controls our Squid, Cache-Control downstream caches
- wfDebug( "$fname: proxy caching with ESI; {$this->mLastModified} **\n", false );
+ wfDebug( __METHOD__ . ": proxy caching with ESI; {$this->mLastModified} **\n", false );
# start with a shorter timeout for initial testing
# header( 'Surrogate-Control: max-age=2678400+2678400, content="ESI/1.0"');
$response->header( 'Surrogate-Control: max-age='.$wgSquidMaxage.'+'.$this->mSquidMaxage.', content="ESI/1.0"');
@@ -570,7 +656,7 @@ class OutputPage {
# to revalidate against the proxy on each visit.
# IMPORTANT! The Squid needs to replace the Cache-Control header with
# Cache-Control: s-maxage=0, must-revalidate, max-age=0
- wfDebug( "$fname: local proxy caching; {$this->mLastModified} **\n", false );
+ wfDebug( __METHOD__ . ": local proxy caching; {$this->mLastModified} **\n", false );
# start with a shorter timeout for initial testing
# header( "Cache-Control: s-maxage=2678400, must-revalidate, max-age=0" );
$response->header( 'Cache-Control: s-maxage='.$this->mSquidMaxage.', must-revalidate, max-age=0' );
@@ -578,13 +664,13 @@ class OutputPage {
} else {
# We do want clients to cache if they can, but they *must* check for updates
# on revisiting the page.
- wfDebug( "$fname: private caching; {$this->mLastModified} **\n", false );
+ wfDebug( __METHOD__ . ": private caching; {$this->mLastModified} **\n", false );
$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}" );
} else {
- wfDebug( "$fname: no caching **\n", false );
+ wfDebug( __METHOD__ . ": no caching **\n", false );
# In general, the absence of a last modified header should be enough to prevent
# the client from using its cache. We send a few other things just to make sure.
@@ -601,14 +687,14 @@ class OutputPage {
public function output() {
global $wgUser, $wgOutputEncoding, $wgRequest;
global $wgContLanguageCode, $wgDebugRedirects, $wgMimeType;
- global $wgJsMimeType, $wgStylePath, $wgUseAjax, $wgAjaxSearch, $wgAjaxWatch;
- global $wgServer, $wgStyleVersion;
+ global $wgJsMimeType, $wgUseAjax, $wgAjaxSearch, $wgAjaxWatch;
+ global $wgServer, $wgEnableMWSuggest;
if( $this->mDoNothing ){
return;
}
- $fname = 'OutputPage::output';
- wfProfileIn( $fname );
+
+ wfProfileIn( __METHOD__ );
if ( '' != $this->mRedirect ) {
# Standards require redirect URLs to be absolute
@@ -631,7 +717,7 @@ class OutputPage {
} else {
$wgRequest->response()->header( 'Location: '.$this->mRedirect );
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return;
}
elseif ( $this->mStatusCode )
@@ -692,20 +778,27 @@ class OutputPage {
$sk = $wgUser->getSkin();
if ( $wgUseAjax ) {
- $this->addScript( "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/ajax.js?$wgStyleVersion\"></script>\n" );
+ $this->addScriptFile( 'ajax.js' );
wfRunHooks( 'AjaxAddScript', array( &$this ) );
if( $wgAjaxSearch && $wgUser->getBoolOption( 'ajaxsearch' ) ) {
- $this->addScript( "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/ajaxsearch.js?$wgStyleVersion\"></script>\n" );
+ $this->addScriptFile( 'ajaxsearch.js' );
$this->addScript( "<script type=\"{$wgJsMimeType}\">hookEvent(\"load\", sajax_onload);</script>\n" );
}
if( $wgAjaxWatch && $wgUser->isLoggedIn() ) {
- $this->addScript( "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/ajaxwatch.js?$wgStyleVersion\"></script>\n" );
+ $this->addScriptFile( 'ajaxwatch.js' );
+ }
+
+ if ( $wgEnableMWSuggest && !$wgUser->getOption( 'disablesuggest', false ) ){
+ $this->addScriptFile( 'mwsuggest.js' );
}
}
-
+
+ if( $wgUser->getBoolOption( 'editsectiononrightclick' ) ) {
+ $this->addScriptFile( 'rightclickedit.js' );
+ }
# Buffer output; final headers may depend on later processing
@@ -720,6 +813,10 @@ class OutputPage {
if ($this->mArticleBodyOnly) {
$this->out($this->mBodytext);
} else {
+ // Hook that allows last minute changes to the output page, e.g.
+ // adding of CSS or Javascript by extensions.
+ wfRunHooks( 'BeforePageDisplay', array( &$this, &$sk ) );
+
wfProfileIn( 'Output-skin' );
$sk->outputPage( $this );
wfProfileOut( 'Output-skin' );
@@ -727,7 +824,7 @@ class OutputPage {
$this->sendCacheControl();
ob_end_flush();
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
}
/**
@@ -767,6 +864,7 @@ class OutputPage {
* @deprecated
*/
public function reportTime() {
+ wfDeprecated( __METHOD__ );
$time = wfReportTime();
return $time;
}
@@ -828,8 +926,8 @@ class OutputPage {
# Don't auto-return to special pages
if( $return ) {
- $return = $wgTitle->getNamespace() > -1 ? $wgTitle->getPrefixedText() : NULL;
- $this->returnToMain( false, $return );
+ $return = $wgTitle->getNamespace() > -1 ? $wgTitle : NULL;
+ $this->returnToMain( null, $return );
}
}
@@ -852,12 +950,12 @@ class OutputPage {
$this->enableClientCache( false );
$this->mRedirect = '';
$this->mBodytext = '';
-
+
array_unshift( $params, 'parse' );
array_unshift( $params, $msg );
$this->addHtml( call_user_func_array( 'wfMsgExt', $params ) );
-
- $this->returnToMain( false );
+
+ $this->returnToMain();
}
/**
@@ -865,7 +963,7 @@ class OutputPage {
*
* @param array $errors Error message keys
*/
- public function showPermissionsErrorPage( $errors )
+ public function showPermissionsErrorPage( $errors, $action = null )
{
global $wgTitle;
@@ -878,14 +976,15 @@ class OutputPage {
$this->enableClientCache( false );
$this->mRedirect = '';
$this->mBodytext = '';
- $this->addWikiText( $this->formatPermissionsErrorMessage( $errors ) );
+ $this->addWikiText( $this->formatPermissionsErrorMessage( $errors, $action ) );
}
/** @deprecated */
public function errorpage( $title, $msg ) {
+ wfDeprecated( __METHOD__ );
throw new ErrorPageError( $title, $msg );
}
-
+
/**
* Display an error page indicating that a given version of MediaWiki is
* required to use it
@@ -942,7 +1041,7 @@ class OutputPage {
$message = wfMsgHtml( 'badaccess-groups', $groups );
}
$this->addHtml( $message );
- $this->returnToMain( false );
+ $this->returnToMain();
}
/**
@@ -973,22 +1072,22 @@ class OutputPage {
}
$skin = $wgUser->getSkin();
-
+
$this->setPageTitle( wfMsg( 'loginreqtitle' ) );
$this->setHtmlTitle( wfMsg( 'errorpagetitle' ) );
$this->setRobotPolicy( 'noindex,nofollow' );
$this->setArticleFlag( false );
-
+
$loginTitle = SpecialPage::getTitleFor( 'Userlogin' );
$loginLink = $skin->makeKnownLinkObj( $loginTitle, wfMsgHtml( 'loginreqlink' ), 'returnto=' . $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
$mainPage = Title::newMainPage();
if( $mainPage->userCanRead() )
- $this->returnToMain( true, $mainPage );
+ $this->returnToMain( null, $mainPage );
}
/** @deprecated */
@@ -1000,8 +1099,14 @@ class OutputPage {
* @param array $errors An array of arrays returned by Title::getUserPermissionsErrors
* @return string The wikitext error-messages, formatted into a list.
*/
- public function formatPermissionsErrorMessage( $errors ) {
- $text = wfMsgNoTrans( 'permissionserrorstext', count( $errors ) ) . "\n\n";
+ public function formatPermissionsErrorMessage( $errors, $action = null ) {
+ if ($action == null) {
+ $text = wfMsgNoTrans( 'permissionserrorstext', count($errors)). "\n\n";
+ } else {
+ $action_desc = wfMsg( "right-$action" );
+ $action_desc[0] = strtolower($action_desc[0]);
+ $text = wfMsgNoTrans( 'permissionserrorstext-withaction', count($errors), $action_desc ) . "\n\n";
+ }
if (count( $errors ) > 1) {
$text .= '<ul class="permissions-errors">' . "\n";
@@ -1014,7 +1119,7 @@ class OutputPage {
}
$text .= '</ul>';
} else {
- $text .= '<div class="permissions-errors">' . call_user_func_array( 'wfMsgNoTrans', $errors[0]) . '</div>';
+ $text .= '<div class="permissions-errors">' . call_user_func_array( 'wfMsgNoTrans', reset( $errors ) ) . '</div>';
}
return $text;
@@ -1039,8 +1144,8 @@ class OutputPage {
* @param bool $protected Is this a permissions error?
* @param array $reasons List of reasons for this error, as returned by Title::getUserPermissionsErrors().
*/
- public function readOnlyPage( $source = null, $protected = false, $reasons = array() ) {
- global $wgUser, $wgReadOnlyFile, $wgReadOnly, $wgTitle;
+ public function readOnlyPage( $source = null, $protected = false, $reasons = array(), $action = null ) {
+ global $wgUser, $wgTitle;
$skin = $wgUser->getSkin();
$this->setRobotpolicy( 'noindex,nofollow' );
@@ -1060,30 +1165,25 @@ class OutputPage {
} else {
$this->setPageTitle( wfMsg( 'badaccess' ) );
}
- $this->addWikiText( $this->formatPermissionsErrorMessage( $reasons ) );
+ $this->addWikiText( $this->formatPermissionsErrorMessage( $reasons, $action ) );
} else {
// Wiki is read only
$this->setPageTitle( wfMsg( 'readonly' ) );
- if ( $wgReadOnly ) {
- $reason = $wgReadOnly;
- } else {
- // Should not happen, user should have called wfReadOnly() first
- $reason = file_get_contents( $wgReadOnlyFile );
- }
+ $reason = wfReadOnlyReason();
$this->addWikiMsg( 'readonlytext', $reason );
}
// Show source, if supplied
if( is_string( $source ) ) {
$this->addWikiMsg( 'viewsourcetext' );
- $text = wfOpenElement( 'textarea',
+ $text = Xml::openElement( 'textarea',
array( 'id' => 'wpTextbox1',
'name' => 'wpTextbox1',
'cols' => $wgUser->getOption( 'cols' ),
'rows' => $wgUser->getOption( 'rows' ),
'readonly' => 'readonly' ) );
$text .= htmlspecialchars( $source );
- $text .= wfCloseElement( 'textarea' );
+ $text .= Xml::closeElement( 'textarea' );
$this->addHTML( $text );
// Show templates used by this article
@@ -1096,37 +1196,43 @@ class OutputPage {
# link to it. After all, you just tried editing it and couldn't, so
# what's there to do there?
if( $wgTitle->exists() ) {
- $this->returnToMain( false, $wgTitle );
+ $this->returnToMain( null, $wgTitle );
}
}
/** @deprecated */
public function fatalError( $message ) {
- throw new FatalError( $message );
+ wfDeprecated( __METHOD__ );
+ throw new FatalError( $message );
}
-
+
/** @deprecated */
public function unexpectedValueError( $name, $val ) {
+ wfDeprecated( __METHOD__ );
throw new FatalError( wfMsg( 'unexpected', $name, $val ) );
}
/** @deprecated */
public function fileCopyError( $old, $new ) {
+ wfDeprecated( __METHOD__ );
throw new FatalError( wfMsg( 'filecopyerror', $old, $new ) );
}
/** @deprecated */
public function fileRenameError( $old, $new ) {
+ wfDeprecated( __METHOD__ );
throw new FatalError( wfMsg( 'filerenameerror', $old, $new ) );
}
/** @deprecated */
public function fileDeleteError( $name ) {
+ wfDeprecated( __METHOD__ );
throw new FatalError( wfMsg( 'filedeleteerror', $name ) );
}
/** @deprecated */
public function fileNotFoundError( $name ) {
+ wfDeprecated( __METHOD__ );
throw new FatalError( wfMsg( 'filenotfound', $name ) );
}
@@ -1179,11 +1285,11 @@ class OutputPage {
*/
public function returnToMain( $unused = null, $returnto = NULL ) {
global $wgRequest;
-
+
if ( $returnto == NULL ) {
$returnto = $wgRequest->getText( 'returnto' );
}
-
+
if ( '' === $returnto ) {
$returnto = Title::newMainPage();
}
@@ -1251,8 +1357,8 @@ class OutputPage {
}
$ret .= "xml:lang=\"$wgContLanguageCode\" lang=\"$wgContLanguageCode\" $rtl>\n";
$ret .= "<head>\n<title>" . htmlspecialchars( $this->getHTMLTitle() ) . "</title>\n";
- array_push( $this->mMetatags, array( "http:Content-type", "$wgMimeType; charset={$wgOutputEncoding}" ) );
-
+ $this->addMeta( "http:Content-type", "$wgMimeType; charset={$wgOutputEncoding}" );
+
$ret .= $this->getHeadLinks();
global $wgStylePath;
if( $this->isPrintable() ) {
@@ -1275,13 +1381,38 @@ class OutputPage {
$ret .= "</head>\n";
return $ret;
}
+
+ protected function addDefaultMeta() {
+ global $wgVersion;
+ $this->addMeta( "generator", "MediaWiki $wgVersion" );
+
+ $p = $this->mRobotpolicy;
+ if( $p !== '' && $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 );
+ }
+
+ if ( count( $this->mKeywords ) > 0 ) {
+ $strip = array(
+ "/<.*?>/" => '',
+ "/_/" => ' '
+ );
+ $this->addMeta( 'keywords', preg_replace(array_keys($strip), array_values($strip),implode( ",", $this->mKeywords ) ) );
+ }
+ }
/**
* @return string HTML tag links to be put in the header.
*/
public function getHeadLinks() {
- global $wgRequest;
- $ret = '';
+ global $wgRequest, $wgFeed;
+
+ // Ideally this should happen earlier, somewhere. :P
+ $this->addDefaultMeta();
+
+ $tags = array();
+
foreach ( $this->mMetatags as $tag ) {
if ( 0 == strcasecmp( 'http:', substr( $tag[0], 0, 5 ) ) ) {
$a = 'http-equiv';
@@ -1289,62 +1420,52 @@ class OutputPage {
} else {
$a = 'name';
}
- $ret .= "<meta $a=\"{$tag[0]}\" content=\"{$tag[1]}\" />\n";
- }
-
- $p = $this->mRobotpolicy;
- if( $p !== '' && $p != 'index,follow' ) {
- // http://www.robotstxt.org/wc/meta-user.html
- // Only show if it's different from the default robots policy
- $ret .= "<meta name=\"robots\" content=\"$p\" />\n";
- }
-
- if ( count( $this->mKeywords ) > 0 ) {
- $strip = array(
- "/<.*?>/" => '',
- "/_/" => ' '
- );
- $ret .= "\t\t<meta name=\"keywords\" content=\"" .
- htmlspecialchars(preg_replace(array_keys($strip), array_values($strip),implode( ",", $this->mKeywords ))) . "\" />\n";
+ $tags[] = Xml::element( 'meta',
+ array(
+ $a => $tag[0],
+ 'content' => $tag[1] ) );
}
foreach ( $this->mLinktags as $tag ) {
- $ret .= "\t\t<link";
- foreach( $tag as $attr => $val ) {
- $ret .= " $attr=\"" . htmlspecialchars( $val ) . "\"";
- }
- $ret .= " />\n";
+ $tags[] = Xml::element( 'link', $tag );
}
-
- foreach( $this->getSyndicationLinks() as $format => $link ) {
- # Use the page name for the title (accessed through $wgTitle since
- # there's no other way). In principle, this could lead to issues
- # with having the same name for different feeds corresponding to
- # the same page, but we can't avoid that at this low a level.
+
+ if( $wgFeed ) {
global $wgTitle;
+ foreach( $this->getSyndicationLinks() as $format => $link ) {
+ # Use the page name for the title (accessed through $wgTitle since
+ # there's no other way). In principle, this could lead to issues
+ # with having the same name for different feeds corresponding to
+ # the same page, but we can't avoid that at this low a level.
+
+ $tags[] = $this->feedLink(
+ $format,
+ $link,
+ wfMsg( "page-{$format}-feed", $wgTitle->getPrefixedText() ) ); # Used messages: 'page-rss-feed' and 'page-atom-feed' (for an easier grep)
+ }
- $ret .= $this->feedLink(
- $format,
- $link,
- wfMsg( "page-{$format}-feed", $wgTitle->getPrefixedText() ) ); # Used messages: 'page-rss-feed' and 'page-atom-feed' (for an easier grep)
- }
+ # 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.
- # Recent changes feed should appear on every page
- # Put it after the per-page feed to avoid changing existing behavior.
- # It's still available, probably via a menu in your browser.
- global $wgSitename;
- $rctitle = SpecialPage::getTitleFor( 'Recentchanges' );
- $ret .= $this->feedLink(
- 'rss',
- $rctitle->getFullURL( 'feed=rss' ),
- wfMsg( 'site-rss-feed', $wgSitename ) );
- $ret .= $this->feedLink(
- 'atom',
- $rctitle->getFullURL( 'feed=atom' ),
- wfMsg( 'site-atom-feed', $wgSitename ) );
+ $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 ) );
+ }
+ }
- return $ret;
+ return implode( "\n\t\t", $tags ) . "\n";
}
-
+
/**
* Return URLs for each supported syndication format for this page.
* @return array associating format keys with URLs
@@ -1352,7 +1473,7 @@ class OutputPage {
public function getSyndicationLinks() {
global $wgTitle, $wgFeedClasses;
$links = array();
-
+
if( $this->isSyndicated() ) {
if( is_string( $this->getFeedAppendQuery() ) ) {
$appendQuery = "&" . $this->getFeedAppendQuery();
@@ -1366,7 +1487,7 @@ class OutputPage {
}
return $links;
}
-
+
/**
* Generate a <link rel/> for an RSS feed.
*/
@@ -1375,7 +1496,7 @@ class OutputPage {
'rel' => 'alternate',
'type' => "application/$type+xml",
'title' => $text,
- 'href' => $url ) ) . "\n";
+ 'href' => $url ) );
}
/**
@@ -1383,7 +1504,7 @@ class OutputPage {
* for when rate limiting has triggered.
*/
public function rateLimited() {
- global $wgOut, $wgTitle;
+ global $wgTitle;
$this->setPageTitle(wfMsg('actionthrottled'));
$this->setRobotPolicy( 'noindex,follow' );
@@ -1394,9 +1515,9 @@ class OutputPage {
$this->setStatusCode(503);
$this->addWikiMsg( 'actionthrottledtext' );
- $this->returnToMain( false, $wgTitle );
+ $this->returnToMain( null, $wgTitle );
}
-
+
/**
* Show an "add new section" link?
*
@@ -1405,7 +1526,7 @@ class OutputPage {
public function showNewSectionLink() {
return $this->mNewSectionLink;
}
-
+
/**
* Show a warning about slave lag
*
@@ -1452,21 +1573,23 @@ class OutputPage {
}
/**
- * This function takes a number of message/argument specifications, wraps them in
+ * This function takes a number of message/argument specifications, wraps them in
* some overall structure, and then parses the result and adds it to the output.
*
- * In the $wrap, $1 is replaced with the first message, $2 with the second, and so
- * on. The subsequent arguments may either be strings, in which case they are the
+ * In the $wrap, $1 is replaced with the first message, $2 with the second, and so
+ * on. The subsequent arguments may either be strings, in which case they are the
* message names, or an arrays, in which case the first element is the message name,
* and subsequent elements are the parameters to that message.
*
* The special named parameter 'options' in a message specification array is passed
- * through to the $options parameter of wfMsgExt().
+ * through to the $options parameter of wfMsgExt().
+ *
+ * Don't use this for messages that are not in users interface language.
*
* For example:
*
* $wgOut->wrapWikiMsg( '<div class="error">$1</div>', 'some-error' );
- *
+ *
* Is equivalent to:
*
* $wgOut->addWikiText( '<div class="error">' . wfMsgNoTrans( 'some-error' ) . '</div>' );
@@ -1491,6 +1614,6 @@ class OutputPage {
}
$s = str_replace( '$' . ($n+1), wfMsgExt( $name, $options, $args ), $s );
}
- $this->addHTML( $this->parse( $s ) );
+ $this->addHTML( $this->parse( $s, /*linestart*/true, /*uilang*/true ) );
}
}
diff --git a/includes/PageHistory.php b/includes/PageHistory.php
index 0c44682e..870b57b7 100644
--- a/includes/PageHistory.php
+++ b/includes/PageHistory.php
@@ -3,6 +3,7 @@
* Page history
*
* Split off from Article.php and Skin.php, 2003-12-22
+ * @file
*/
/**
@@ -17,7 +18,7 @@
class PageHistory {
const DIR_PREV = 0;
const DIR_NEXT = 1;
-
+
var $mArticle, $mTitle, $mSkin;
var $lastdate;
var $linesonpage;
@@ -37,6 +38,28 @@ class PageHistory {
$this->mTitle =& $article->mTitle;
$this->mNotificationTimestamp = NULL;
$this->mSkin = $wgUser->getSkin();
+ $this->preCacheMessages();
+ }
+
+ function getArticle() {
+ return $this->mArticle;
+ }
+
+ function getTitle() {
+ return $this->mTitle;
+ }
+
+ /**
+ * As we use the same small set of messages in various methods and that
+ * they are called often, we call them once and save them in $this->message
+ */
+ function preCacheMessages() {
+ // Precache various messages
+ if( !isset( $this->message ) ) {
+ foreach( explode(' ', 'cur last rev-delundel' ) as $msg ) {
+ $this->message[$msg] = wfMsgExt( $msg, array( 'escape') );
+ }
+ }
}
/**
@@ -51,12 +74,11 @@ class PageHistory {
* Allow client caching.
*/
- if( $wgOut->checkLastModified( $this->mArticle->getTimestamp() ) )
+ if( $wgOut->checkLastModified( $this->mArticle->getTouched() ) )
/* Client cache fresh and headers sent, nothing more to do. */
return;
- $fname = 'PageHistory::history';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
/*
* Setup page variables.
@@ -68,6 +90,7 @@ class PageHistory {
$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() );
@@ -75,7 +98,7 @@ class PageHistory {
$feedType = $wgRequest->getVal( 'feed' );
if( $feedType ) {
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $this->feed( $feedType );
}
@@ -84,7 +107,7 @@ class PageHistory {
*/
if( !$this->mTitle->exists() ) {
$wgOut->addWikiMsg( 'nohistory' );
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return;
}
@@ -94,25 +117,29 @@ class PageHistory {
*/
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;
}
wfRunHooks( 'PageHistoryBeforeList', array( &$this->mArticle ) );
- /**
+ /**
* Do the list
*/
$pager = new PageHistoryPager( $this );
$this->linesonpage = $pager->getNumRows();
$wgOut->addHTML(
- $pager->getNavigationBar() .
- $this->beginHistoryList() .
+ $pager->getNavigationBar() .
+ $this->beginHistoryList() .
$pager->getBody() .
$this->endHistoryList() .
$pager->getNavigationBar()
);
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
}
/**
@@ -121,25 +148,11 @@ class PageHistory {
* @return string HTML output
*/
function beginHistoryList() {
- global $wgTitle;
+ global $wgTitle, $wgScript;
$this->lastdate = '';
$s = wfMsgExt( 'histlegend', array( 'parse') );
- $s .= '<form action="' . $wgTitle->escapeLocalURL( '-' ) . '" method="get">';
- $prefixedkey = htmlspecialchars($wgTitle->getPrefixedDbKey());
-
- // The following line is SUPPOSED to have double-quotes around the
- // $prefixedkey variable, because htmlspecialchars() doesn't escape
- // single-quotes.
- //
- // On at least two occasions people have changed it to single-quotes,
- // which creates invalid HTML and incorrect display of the resulting
- // link.
- //
- // Please do not break this a third time. Thank you for your kind
- // consideration and cooperation.
- //
- $s .= "<input type='hidden' name='title' value=\"{$prefixedkey}\" />\n";
-
+ $s .= Xml::openElement( 'form', array( 'action' => $wgScript ) );
+ $s .= Xml::hidden( 'title', $wgTitle->getPrefixedDbKey() );
$s .= $this->submitButton();
$s .= '<ul id="pagehistory">' . "\n";
return $s;
@@ -183,8 +196,8 @@ class PageHistory {
*
* @todo document some more, and maybe clean up the code (some params redundant?)
*
- * @param object $row The database row corresponding to the line (or is it the previous line?).
- * @param object $next The database row corresponding to the next line (or is it this one?).
+ * @param Row $row The database row corresponding to the previous line.
+ * @param mixed $next The database row corresponding to the next line.
* @param int $counter Apparently a counter of what row number we're at, counted from the top row = 1.
* @param $notificationtimestamp
* @param bool $latest Whether this row corresponds to the page's latest revision.
@@ -196,71 +209,57 @@ class PageHistory {
$rev = new Revision( $row );
$rev->setTitle( $this->mTitle );
- $s = '<li>';
+ $s = '';
$curlink = $this->curLink( $rev, $latest );
$lastlink = $this->lastLink( $rev, $next, $counter );
$arbitrary = $this->diffButtons( $rev, $firstInList, $counter );
$link = $this->revLink( $rev );
-
- $user = $this->mSkin->userLink( $rev->getUser(), $rev->getUserText() )
- . $this->mSkin->userToolLinks( $rev->getUser(), $rev->getUserText() );
-
+
$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
- $del = wfMsgHtml( 'rev-delundel' );
+ // 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
- $del = wfMsgHtml( 'rev-delundel' );
+ $del = $this->message['rev-delundel'];
} else {
$del = $this->mSkin->makeKnownLinkObj( $revdel,
- wfMsg( '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>";
}
- $s .= " (<small>$del</small>) ";
+ $s .= " <tt>(<small>$del</small>)</tt> ";
}
-
+
$s .= " $link";
- #getUser is safe, but this avoids making the invalid untargeted contribs links
- if( $row->rev_deleted & Revision::DELETED_USER ) {
- $user = '<span class="history-deleted">' . wfMsg('rev-deleted-user') . '</span>';
- }
- $s .= " <span class='history-user'>$user</span>";
+ $s .= " <span class='history-user'>" . $this->mSkin->revUserTools( $rev, true ) . "</span>";
if( $row->rev_minor_edit ) {
- $s .= ' ' . wfElement( 'span', array( 'class' => 'minor' ), wfMsg( 'minoreditletter') );
+ $s .= ' ' . Xml::element( 'span', array( 'class' => 'minor' ), wfMsg( 'minoreditletter') );
}
- if ( !is_null( $size = $rev->getSize() ) ) {
- if ( $size == 0 )
- $stxt = wfMsgHtml( 'historyempty' );
- else
- $stxt = wfMsgExt( 'historysize', array( 'parsemag' ), $wgLang->formatNum( $size ) );
- $s .= " <span class=\"history-size\">$stxt</span>";
+ if ( !is_null( $size = $rev->getSize() ) && $rev->userCan( Revision::DELETED_TEXT ) ) {
+ $s .= ' ' . $this->mSkin->formatRevisionSize( $size );
}
- #getComment is safe, but this is better formatted
- if( $rev->isDeleted( Revision::DELETED_COMMENT ) ) {
- $s .= " <span class=\"history-deleted\"><span class=\"comment\">" .
- wfMsgHtml( 'rev-deleted-comment' ) . "</span></span>";
- } else {
- $s .= $this->mSkin->revComment( $rev );
- }
-
+ $s .= $this->mSkin->revComment( $rev, false, true );
+
if ($notificationtimestamp && ($row->rev_timestamp >= $notificationtimestamp)) {
$s .= ' <span class="updatedmarker">' . wfMsgHtml( 'updatedmarker' ) . '</span>';
}
#add blurb about text having been deleted
- if( $row->rev_deleted & Revision::DELETED_TEXT ) {
- $s .= ' ' . wfMsgHtml( 'deletedrev' );
+ if( $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 )
@@ -270,7 +269,9 @@ class PageHistory {
. '</span>';
}
- if( $this->mTitle->quickUserCan( 'edit' ) ) {
+ if( $this->mTitle->quickUserCan( 'edit' ) &&
+ !$rev->isDeleted( Revision::DELETED_TEXT ) &&
+ !$next->rev_deleted & Revision::DELETED_TEXT ) {
$undolink = $this->mSkin->makeKnownLinkObj(
$this->mTitle,
wfMsgHtml( 'editundo' ),
@@ -279,19 +280,21 @@ class PageHistory {
$tools[] = "<span class=\"mw-history-undo\">{$undolink}</span>";
}
}
-
+
if( $tools ) {
$s .= ' (' . implode( ' | ', $tools ) . ')';
}
-
- wfRunHooks( 'PageHistoryLineEnding', array( &$row , &$s ) );
-
- $s .= "</li>\n";
- return $s;
+ wfRunHooks( 'PageHistoryLineEnding', array( $this, &$row , &$s ) );
+
+ return "<li>$s</li>\n";
}
-
- /** @todo document */
+
+ /**
+ * 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 );
@@ -307,9 +310,14 @@ class PageHistory {
return $link;
}
- /** @todo document */
+ /**
+ * 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 = wfMsgExt( 'cur', array( 'escape') );
+ $cur = $this->message['cur'];
if( $latest || !$rev->userCan( Revision::DELETED_TEXT ) ) {
return $cur;
} else {
@@ -320,25 +328,33 @@ class PageHistory {
}
}
- /** @todo document */
- function lastLink( $rev, $next, $counter ) {
- $last = wfMsgExt( 'last', array( 'escape' ) );
- if ( is_null( $next ) ) {
+ /**
+ * 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"
+ $nextRev = is_object($next) ? new Revision( $next ) : $next;
+ if( is_null($next) ) {
# Probably no next row
return $last;
- } elseif ( $next === 'unknown' ) {
+ } elseif( $next === 'unknown' ) {
# Next row probably exists but is unknown, use an oldid=prev link
return $this->mSkin->makeKnownLinkObj(
$this->mTitle,
$last,
- "diff=" . $rev->getId() . "&oldid=prev" );
- } elseif( !$rev->userCan( Revision::DELETED_TEXT ) ) {
+ "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=" . $rev->getId() . "&oldid={$next->rev_id}"
+ "diff=" . $prevRev->getId() . "&oldid={$next->rev_id}"
/*,
'',
'',
@@ -399,23 +415,21 @@ class PageHistory {
function getLatestId() {
if( is_null( $this->mLatestId ) ) {
$id = $this->mTitle->getArticleID();
- $db = wfGetDB(DB_SLAVE);
+ $db = wfGetDB( DB_SLAVE );
$this->mLatestId = $db->selectField( 'page',
"page_latest",
array( 'page_id' => $id ),
- 'PageHistory::getLatestID' );
+ __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
+ * direction. This is now only used by the feeds. It was previously
* used by the main UI but that's now handled by the pager.
*/
function fetchRevisions($limit, $offset, $direction) {
- $fname = 'PageHistory::fetchRevisions';
-
$dbr = wfGetDB( DB_SLAVE );
if ($direction == PageHistory::DIR_PREV)
@@ -434,7 +448,7 @@ class PageHistory {
'revision',
Revision::selectFields(),
array_merge(array("rev_page=$page_id"), $offsets),
- $fname,
+ __METHOD__,
array('ORDER BY' => "rev_timestamp $dirs",
'USE INDEX' => 'page_timestamp', 'LIMIT' => $limit)
);
@@ -449,7 +463,6 @@ class PageHistory {
/** @todo document */
function getNotificationTimestamp() {
global $wgUser, $wgShowUpdatedMarker;
- $fname = 'PageHistory::getNotficationTimestamp';
if ($this->mNotificationTimestamp !== NULL)
return $this->mNotificationTimestamp;
@@ -464,10 +477,10 @@ class PageHistory {
'wl_notificationtimestamp',
array( 'wl_namespace' => $this->mTitle->getNamespace(),
'wl_title' => $this->mTitle->getDBkey(),
- 'wl_user' => $wgUser->getID()
+ 'wl_user' => $wgUser->getId()
),
- $fname);
-
+ __METHOD__ );
+
// Don't use the special value reserved for telling whether the field is filled
if ( is_null( $this->mNotificationTimestamp ) ) {
$this->mNotificationTimestamp = false;
@@ -475,28 +488,31 @@ class PageHistory {
return $this->mNotificationTimestamp;
}
-
+
/**
* Output a subscription feed listing recent edits to this page.
* @param string $type
*/
function feed( $type ) {
- require_once 'SpecialRecentchanges.php';
-
- global $wgFeedClasses;
- if( !isset( $wgFeedClasses[$type] ) ) {
- global $wgOut;
- $wgOut->addWikiMsg( 'feed-invalid' );
+ global $wgFeedClasses, $wgRequest, $wgFeedLimit;
+ 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' ) );
- $items = $this->fetchRevisions(10, 0, PageHistory::DIR_NEXT);
+ // Get a limit on number of feed entries. Provide a sane default
+ // of 10 if none is defined (but limit to $wgFeedLimit max)
+ $limit = $wgRequest->getInt( 'limit', 10 );
+ if( $limit > $wgFeedLimit || $limit < 1 ) {
+ $limit = 10;
+ }
+ $items = $this->fetchRevisions($limit, 0, PageHistory::DIR_NEXT);
+
$feed->outHeader();
if( $items ) {
foreach( $items as $row ) {
@@ -507,7 +523,7 @@ class PageHistory {
}
$feed->outFooter();
}
-
+
function feedEmpty() {
global $wgOut;
return new FeedItem(
@@ -518,7 +534,7 @@ class PageHistory {
'',
$this->mTitle->getTalkPage()->getFullUrl() );
}
-
+
/**
* Generate a FeedItem object from a given revision table row
* Borrows Recent Changes' feed generation functions for formatting;
@@ -530,12 +546,12 @@ class PageHistory {
function feedItem( $row ) {
$rev = new Revision( $row );
$rev->setTitle( $this->mTitle );
- $text = rcFormatDiffRow( $this->mTitle,
+ $text = FeedUtils::formatDiffRow( $this->mTitle,
$this->mTitle->getPreviousRevisionID( $rev->getId() ),
$rev->getId(),
$rev->getTimestamp(),
$rev->getComment() );
-
+
if( $rev->getComment() == '' ) {
global $wgContLang;
$title = wfMsgForContent( 'history-feed-item-nocomment',
@@ -553,7 +569,7 @@ class PageHistory {
$rev->getUserText(),
$this->mTitle->getTalkPage()->getFullUrl() );
}
-
+
/**
* Quickie hack... strip out wikilinks to more legible form from the comment.
*/
@@ -564,23 +580,25 @@ class PageHistory {
/**
- * @addtogroup Pager
+ * @ingroup Pager
*/
class PageHistoryPager extends ReverseChronologicalPager {
public $mLastRow = false, $mPageHistory;
-
+
function __construct( $pageHistory ) {
parent::__construct();
$this->mPageHistory = $pageHistory;
}
function getQueryInfo() {
- return array(
- 'tables' => 'revision',
- 'fields' => Revision::selectFields(),
- 'conds' => array('rev_page' => $this->mPageHistory->mTitle->getArticleID() ),
- 'options' => array( 'USE INDEX' => 'page_timestamp' )
+ $queryInfo = array(
+ 'tables' => array('revision'),
+ 'fields' => Revision::selectFields(),
+ 'conds' => array('rev_page' => $this->mPageHistory->mTitle->getArticleID() ),
+ 'options' => array( 'USE INDEX' => array('revision' => 'page_timestamp') )
);
+ wfRunHooks( 'PageHistoryPager::getQueryInfo', array( &$this, &$queryInfo ) );
+ return $queryInfo;
}
function getIndexField() {
@@ -589,9 +607,9 @@ class PageHistoryPager extends ReverseChronologicalPager {
function formatRow( $row ) {
if ( $this->mLastRow ) {
- $latest = $this->mCounter == 1 && $this->mOffset == '';
+ $latest = $this->mCounter == 1 && $this->mIsFirst;
$firstInList = $this->mCounter == 1;
- $s = $this->mPageHistory->historyLine( $this->mLastRow, $row, $this->mCounter++,
+ $s = $this->mPageHistory->historyLine( $this->mLastRow, $row, $this->mCounter++,
$this->mPageHistory->getNotificationTimestamp(), $latest, $firstInList );
} else {
$s = '';
@@ -599,7 +617,7 @@ class PageHistoryPager extends ReverseChronologicalPager {
$this->mLastRow = $row;
return $s;
}
-
+
function getStartBody() {
$this->mLastRow = false;
$this->mCounter = 1;
@@ -608,7 +626,7 @@ class PageHistoryPager extends ReverseChronologicalPager {
function getEndBody() {
if ( $this->mLastRow ) {
- $latest = $this->mCounter == 1 && $this->mOffset == 0;
+ $latest = $this->mCounter == 1 && $this->mIsFirst;
$firstInList = $this->mCounter == 1;
if ( $this->mIsBackwards ) {
# Next row is unknown, but for UI reasons, probably exists if an offset has been specified
@@ -621,7 +639,7 @@ class PageHistoryPager extends ReverseChronologicalPager {
# The next row is the past-the-end row
$next = $this->mPastTheEndRow;
}
- $s = $this->mPageHistory->historyLine( $this->mLastRow, $next, $this->mCounter++,
+ $s = $this->mPageHistory->historyLine( $this->mLastRow, $next, $this->mCounter++,
$this->mPageHistory->getNotificationTimestamp(), $latest, $firstInList );
} else {
$s = '';
diff --git a/includes/PageQueryPage.php b/includes/PageQueryPage.php
index 53b7765e..0d1789ee 100644
--- a/includes/PageQueryPage.php
+++ b/includes/PageQueryPage.php
@@ -3,16 +3,15 @@
/**
* Variant of QueryPage which formats the result as a simple link to the page
*
- * @package MediaWiki
- * @addtogroup SpecialPage
+ * @ingroup SpecialPage
*/
class PageQueryPage extends QueryPage {
/**
* Format the result as a simple link to the page
*
- * @param Skin $skin
- * @param object $row Result row
+ * @param $skin Skin
+ * @param $row Object: result row
* @return string
*/
public function formatResult( $skin, $row ) {
@@ -22,5 +21,3 @@ class PageQueryPage extends QueryPage {
htmlspecialchars( $wgContLang->convert( $title->getPrefixedText() ) ) );
}
}
-
-
diff --git a/includes/Pager.php b/includes/Pager.php
index ed7086b4..62c4e551 100644
--- a/includes/Pager.php
+++ b/includes/Pager.php
@@ -1,8 +1,14 @@
<?php
+/**
+ * @defgroup Pager Pager
+ *
+ * @file
+ * @ingroup Pager
+ */
/**
* Basic pager interface.
- * @addtogroup Pager
+ * @ingroup Pager
*/
interface Pager {
function getNavigationBar();
@@ -10,18 +16,18 @@ interface Pager {
}
/**
- * IndexPager is an efficient pager which uses a (roughly unique) index in the
- * data set to implement paging, rather than a "LIMIT offset,limit" clause.
+ * IndexPager is an efficient pager which uses a (roughly unique) index in the
+ * data set to implement paging, rather than a "LIMIT offset,limit" clause.
* In MySQL, such a limit/offset clause requires counting through the
* specified number of offset rows to find the desired data, which can be
* expensive for large offsets.
- *
+ *
* ReverseChronologicalPager is a child class of the abstract IndexPager, and
* contains some formatting and display code which is specific to the use of
* timestamps as indexes. Here is a synopsis of its operation:
- *
+ *
* * The query is specified by the offset, limit and direction (dir)
- * parameters, in addition to any subclass-specific parameters.
+ * parameters, in addition to any subclass-specific parameters.
* * The offset is the non-inclusive start of the DB query. A row with an
* index value equal to the offset will never be shown.
* * The query may either be done backwards, where the rows are returned by
@@ -29,27 +35,27 @@ interface Pager {
* user, or forwards. This is specified by the "dir" parameter, dir=prev
* means backwards, anything else means forwards. The offset value
* specifies the start of the database result set, which may be either
- * the start or end of the displayed data set. This allows "previous"
+ * the start or end of the displayed data set. This allows "previous"
* links to be implemented without knowledge of the index value at the
- * start of the previous page.
+ * start of the previous page.
* * An additional row beyond the user-specified limit is always requested.
* This allows us to tell whether we should display a "next" link in the
* case of forwards mode, or a "previous" link in the case of backwards
* mode. Determining whether to display the other link (the one for the
* page before the start of the database result set) can be done
- * heuristically by examining the offset.
+ * heuristically by examining the offset.
*
* * An empty offset indicates that the offset condition should be omitted
* from the query. This naturally produces either the first page or the
- * last page depending on the dir parameter.
+ * last page depending on the dir parameter.
*
* Subclassing the pager to implement concrete functionality should be fairly
- * simple, please see the examples in PageHistory.php and
+ * simple, please see the examples in PageHistory.php and
* SpecialIpblocklist.php. You just need to override formatRow(),
* getQueryInfo() and getIndexField(). Don't forget to call the parent
* constructor if you override it.
*
- * @addtogroup Pager
+ * @ingroup Pager
*/
abstract class IndexPager implements Pager {
public $mRequest;
@@ -60,39 +66,74 @@ abstract class IndexPager implements Pager {
public $mDb;
public $mPastTheEndRow;
+ /**
+ * The index to actually be used for ordering. This is a single string e-
+ * ven if multiple orderings are supported.
+ */
protected $mIndexField;
-
+ /** For pages that support multiple types of ordering, which one to use. */
+ protected $mOrderType;
/**
- * Default query direction. false for ascending, true for descending
+ * $mDefaultDirection gives the direction to use when sorting results:
+ * false for ascending, true for descending. If $mIsBackwards is set, we
+ * start from the opposite end, but we still sort the page itself according
+ * to $mDefaultDirection. E.g., if $mDefaultDirection is false but we're
+ * going backwards, we'll display the last page of results, but the last
+ * result will be at the bottom, not the top.
+ *
+ * Like $mIndexField, $mDefaultDirection will be a single value even if the
+ * class supports multiple default directions for different order types.
*/
- public $mDefaultDirection = false;
+ public $mDefaultDirection;
+ public $mIsBackwards;
/**
* Result object for the query. Warning: seek before use.
*/
public $mResult;
- function __construct() {
+ public function __construct() {
global $wgRequest, $wgUser;
$this->mRequest = $wgRequest;
-
+
# NB: the offset is quoted, not validated. It is treated as an
# arbitrary string to support the widest variety of index types. Be
# careful outputting it into HTML!
$this->mOffset = $this->mRequest->getText( 'offset' );
-
+
# Use consistent behavior for the limit options
$this->mDefaultLimit = intval( $wgUser->getOption( 'rclimit' ) );
list( $this->mLimit, /* $offset */ ) = $this->mRequest->getLimitOffset();
-
+
$this->mIsBackwards = ( $this->mRequest->getVal( 'dir' ) == 'prev' );
- $this->mIndexField = $this->getIndexField();
$this->mDb = wfGetDB( DB_SLAVE );
+
+ $index = $this->getIndexField();
+ $order = $this->mRequest->getVal( 'order' );
+ if( is_array( $index ) && isset( $index[$order] ) ) {
+ $this->mOrderType = $order;
+ $this->mIndexField = $index[$order];
+ } elseif( is_array( $index ) ) {
+ # First element is the default
+ reset( $index );
+ list( $this->mOrderType, $this->mIndexField ) = each( $index );
+ } else {
+ # $index is not an array
+ $this->mOrderType = null;
+ $this->mIndexField = $index;
+ }
+
+ if( !isset( $this->mDefaultDirection ) ) {
+ $dir = $this->getDefaultDirections();
+ $this->mDefaultDirection = is_array( $dir )
+ ? $dir[$this->mOrderType]
+ : $dir;
+ }
}
/**
- * Do the query, using information from the object context. This function
- * has been kept minimal to make it overridable if necessary, to allow for
+ * Do the query, using information from the object context. This function
+ * has been kept minimal to make it overridable if necessary, to allow for
* result sets formed from multiple DB queries.
*/
function doQuery() {
@@ -107,7 +148,7 @@ abstract class IndexPager implements Pager {
$this->mResult = $this->reallyDoQuery( $this->mOffset, $queryLimit, $descending );
$this->extractResultInfo( $this->mOffset, $queryLimit, $this->mResult );
$this->mQueryDone = true;
-
+
$this->preprocessResults( $this->mResult );
$this->mResult->rewind(); // Paranoia
@@ -115,7 +156,7 @@ abstract class IndexPager implements Pager {
}
/**
- * Extract some useful data from the result object for use by
+ * Extract some useful data from the result object for use by
* the navigation bar, put it into $this
*/
function extractResultInfo( $offset, $limit, ResultWrapper $res ) {
@@ -180,6 +221,7 @@ abstract class IndexPager implements Pager {
$fields = $info['fields'];
$conds = isset( $info['conds'] ) ? $info['conds'] : array();
$options = isset( $info['options'] ) ? $info['options'] : array();
+ $join_conds = isset( $info['join_conds'] ) ? $info['join_conds'] : array();
if ( $descending ) {
$options['ORDER BY'] = $this->mIndexField;
$operator = '>';
@@ -191,7 +233,7 @@ abstract class IndexPager implements Pager {
$conds[] = $this->mIndexField . $operator . $this->mDb->addQuotes( $offset );
}
$options['LIMIT'] = intval( $limit );
- $res = $this->mDb->select( $tables, $fields, $conds, $fname, $options );
+ $res = $this->mDb->select( $tables, $fields, $conds, $fname, $options, $join_conds );
return new ResultWrapper( $this->mDb, $res );
}
@@ -203,7 +245,7 @@ abstract class IndexPager implements Pager {
protected function preprocessResults( $result ) {}
/**
- * Get the formatted result list. Calls getStartBody(), formatRow() and
+ * Get the formatted result list. Calls getStartBody(), formatRow() and
* getEndBody(), concatenates the results and returns them.
*/
function getBody() {
@@ -238,17 +280,25 @@ abstract class IndexPager implements Pager {
/**
* Make a self-link
*/
- function makeLink($text, $query = NULL) {
+ function makeLink($text, $query = null, $type=null) {
if ( $query === null ) {
return $text;
+ }
+ if( $type == 'prev' || $type == 'next' ) {
+ $attrs = "rel=\"$type\"";
+ } elseif( $type == 'first' ) {
+ $attrs = "rel=\"start\"";
} else {
- return $this->getSkin()->makeKnownLinkObj( $this->getTitle(), $text,
- wfArrayToCGI( $query, $this->getDefaultQuery() ) );
+ # HTML 4 has no rel="end" . . .
+ $attrs = '';
}
+ return $this->getSkin()->makeKnownLinkObj( $this->getTitle(), $text,
+ wfArrayToCGI( $query, $this->getDefaultQuery() ), '', '',
+ $attrs );
}
/**
- * Hook into getBody(), allows text to be inserted at the start. This
+ * Hook into getBody(), allows text to be inserted at the start. This
* will be called even if there are no rows in the result set.
*/
function getStartBody() {
@@ -263,15 +313,15 @@ abstract class IndexPager implements Pager {
}
/**
- * Hook into getBody(), for the bit between the start and the
+ * Hook into getBody(), for the bit between the start and the
* end when there are no rows
*/
function getEmptyBody() {
return '';
}
-
+
/**
- * Title used for self-links. Override this if you want to be able to
+ * Title used for self-links. Override this if you want to be able to
* use a title other than $wgTitle
*/
function getTitle() {
@@ -290,8 +340,8 @@ abstract class IndexPager implements Pager {
}
/**
- * Get an array of query parameters that should be put into self-links.
- * By default, all parameters passed in the URL are used, except for a
+ * Get an array of query parameters that should be put into self-links.
+ * By default, all parameters passed in the URL are used, except for a
* short blacklist.
*/
function getDefaultQuery() {
@@ -301,6 +351,7 @@ abstract class IndexPager implements Pager {
unset( $this->mDefaultQuery['dir'] );
unset( $this->mDefaultQuery['offset'] );
unset( $this->mDefaultQuery['limit'] );
+ unset( $this->mDefaultQuery['order'] );
}
return $this->mDefaultQuery;
}
@@ -316,16 +367,16 @@ abstract class IndexPager implements Pager {
}
/**
- * Get a query array for the prev, next, first and last links.
+ * Get a URL query array for the prev, next, first and last links.
*/
function getPagingQueries() {
if ( !$this->mQueryDone ) {
$this->doQuery();
}
-
+
# Don't announce the limit everywhere if it's the default
$urlLimit = $this->mLimit == $this->mDefaultLimit ? '' : $this->mLimit;
-
+
if ( $this->mIsFirst ) {
$prev = false;
$first = false;
@@ -354,7 +405,7 @@ abstract class IndexPager implements Pager {
$links = array();
foreach ( $queries as $type => $query ) {
if ( $query !== false ) {
- $links[$type] = $this->makeLink( $linkTexts[$type], $queries[$type] );
+ $links[$type] = $this->makeLink( $linkTexts[$type], $queries[$type], $type );
} elseif ( isset( $disabledTexts[$type] ) ) {
$links[$type] = $disabledTexts[$type];
} else {
@@ -380,15 +431,15 @@ abstract class IndexPager implements Pager {
}
/**
- * Abstract formatting function. This should return an HTML string
+ * Abstract formatting function. This should return an HTML string
* representing the result row $row. Rows will be concatenated and
* returned by getBody()
*/
abstract function formatRow( $row );
/**
- * This function should be overridden to provide all parameters
- * needed for the main paged query. It returns an associative
+ * This function should be overridden to provide all parameters
+ * needed for the main paged query. It returns an associative
* array with the following elements:
* tables => Table(s) for passing to Database::select()
* fields => Field(s) for passing to Database::select(), may be *
@@ -398,51 +449,118 @@ abstract class IndexPager implements Pager {
abstract function getQueryInfo();
/**
- * This function should be overridden to return the name of the
- * index field.
+ * This function should be overridden to return the name of the index fi-
+ * eld. If the pager supports multiple orders, it may return an array of
+ * 'querykey' => 'indexfield' pairs, so that a request with &count=querykey
+ * will use indexfield to sort. In this case, the first returned key is
+ * the default.
+ *
+ * Needless to say, it's really not a good idea to use a non-unique index
+ * for this! That won't page right.
*/
abstract function getIndexField();
+
+ /**
+ * Return the default sorting direction: false for ascending, true for de-
+ * scending. You can also have an associative array of ordertype => dir,
+ * if multiple order types are supported. In this case getIndexField()
+ * must return an array, and the keys of that must exactly match the keys
+ * of this.
+ *
+ * For backward compatibility, this method's return value will be ignored
+ * if $this->mDefaultDirection is already set when the constructor is
+ * called, for instance if it's statically initialized. In that case the
+ * value of that variable (which must be a boolean) will be used.
+ *
+ * Note that despite its name, this does not return the value of the
+ * $this->mDefaultDirection member variable. That's the default for this
+ * particular instantiation, which is a single value. This is the set of
+ * all defaults for the class.
+ */
+ protected function getDefaultDirections() { return false; }
}
/**
* IndexPager with an alphabetic list and a formatted navigation bar
- * @addtogroup Pager
+ * @ingroup Pager
*/
abstract class AlphabeticPager extends IndexPager {
- public $mDefaultDirection = false;
-
- function __construct() {
- parent::__construct();
- }
-
- /**
- * Shamelessly stolen bits from ReverseChronologicalPager, d
- * didn't want to do class magic as may be still revamped
+ /**
+ * Shamelessly stolen bits from ReverseChronologicalPager,
+ * didn't want to do class magic as may be still revamped
*/
function getNavigationBar() {
global $wgLang;
+ if( isset( $this->mNavigationBar ) ) {
+ return $this->mNavigationBar;
+ }
+
+ $opts = array( 'parsemag', 'escapenoentities' );
$linkTexts = array(
- 'prev' => wfMsgHtml( 'prevn', $wgLang->formatNum( $this->mLimit ) ),
- 'next' => wfMsgHtml( 'nextn', $wgLang->formatNum($this->mLimit ) ),
- 'first' => wfMsgHtml( 'page_first' ), /* Introduced the message */
- 'last' => wfMsgHtml( 'page_last' ) /* Introduced the message */
+ 'prev' => wfMsgExt( 'prevn', $opts, $wgLang->formatNum( $this->mLimit ) ),
+ 'next' => wfMsgExt( 'nextn', $opts, $wgLang->formatNum($this->mLimit ) ),
+ 'first' => wfMsgExt( 'page_first', $opts ),
+ 'last' => wfMsgExt( 'page_last', $opts )
);
$pagingLinks = $this->getPagingLinks( $linkTexts );
$limitLinks = $this->getLimitLinks();
$limits = implode( ' | ', $limitLinks );
- $this->mNavigationBar = "({$pagingLinks['first']} | {$pagingLinks['last']}) " . wfMsgHtml("viewprevnext", $pagingLinks['prev'], $pagingLinks['next'], $limits);
+ $this->mNavigationBar =
+ "({$pagingLinks['first']} | {$pagingLinks['last']}) " .
+ wfMsgHtml( 'viewprevnext', $pagingLinks['prev'],
+ $pagingLinks['next'], $limits );
+
+ if( !is_array( $this->getIndexField() ) ) {
+ # Early return to avoid undue nesting
+ return $this->mNavigationBar;
+ }
+
+ $extra = '';
+ $first = true;
+ $msgs = $this->getOrderTypeMessages();
+ foreach( array_keys( $msgs ) as $order ) {
+ if( $first ) {
+ $first = false;
+ } else {
+ $extra .= ' | ';
+ }
+
+ if( $order == $this->mOrderType ) {
+ $extra .= wfMsgHTML( $msgs[$order] );
+ } else {
+ $extra .= $this->makeLink(
+ wfMsgHTML( $msgs[$order] ),
+ array( 'order' => $order )
+ );
+ }
+ }
+
+ if( $extra !== '' ) {
+ $this->mNavigationBar .= " ($extra)";
+ }
+
return $this->mNavigationBar;
+ }
+ /**
+ * If this supports multiple order type messages, give the message key for
+ * enabling each one in getNavigationBar. The return type is an associa-
+ * tive array whose keys must exactly match the keys of the array returned
+ * by getIndexField(), and whose values are message keys.
+ * @return array
+ */
+ protected function getOrderTypeMessages() {
+ return null;
}
}
/**
* IndexPager with a formatted navigation bar
- * @addtogroup Pager
+ * @ingroup Pager
*/
abstract class ReverseChronologicalPager extends IndexPager {
public $mDefaultDirection = true;
@@ -469,7 +587,7 @@ abstract class ReverseChronologicalPager extends IndexPager {
$limitLinks = $this->getLimitLinks();
$limits = implode( ' | ', $limitLinks );
- $this->mNavigationBar = "({$pagingLinks['first']} | {$pagingLinks['last']}) " .
+ $this->mNavigationBar = "({$pagingLinks['first']} | {$pagingLinks['last']}) " .
wfMsgHtml("viewprevnext", $pagingLinks['prev'], $pagingLinks['next'], $limits);
return $this->mNavigationBar;
}
@@ -477,7 +595,7 @@ abstract class ReverseChronologicalPager extends IndexPager {
/**
* Table-based display with a user-selectable sort order
- * @addtogroup Pager
+ * @ingroup Pager
*/
abstract class TablePager extends IndexPager {
var $mSort;
@@ -502,7 +620,7 @@ abstract class TablePager extends IndexPager {
global $wgStylePath;
$tableClass = htmlspecialchars( $this->getTableClass() );
$sortClass = htmlspecialchars( $this->getSortHeaderClass() );
-
+
$s = "<table border='1' class=\"$tableClass\"><thead><tr>\n";
$fields = $this->getFieldNames();
@@ -529,7 +647,7 @@ abstract class TablePager extends IndexPager {
$alt = htmlspecialchars( wfMsg( 'ascending_abbrev' ) );
}
$image = htmlspecialchars( "$wgStylePath/common/images/$image" );
- $link = $this->makeLink(
+ $link = $this->makeLink(
"<img width=\"12\" height=\"12\" alt=\"$alt\" src=\"$image\" />" .
htmlspecialchars( $name ), $query );
$s .= "<th class=\"$sortClass\">$link</th>\n";
@@ -541,7 +659,7 @@ abstract class TablePager extends IndexPager {
}
}
$s .= "</tr></thead><tbody>\n";
- return $s;
+ return $s;
}
function getEndBody() {
@@ -647,8 +765,8 @@ abstract class TablePager extends IndexPager {
}
/**
- * Get <input type="hidden"> elements for use in a method="get" form.
- * Resubmits all defined elements of the $_GET array, except for a
+ * Get <input type="hidden"> elements for use in a method="get" form.
+ * Resubmits all defined elements of the $_GET array, except for a
* blacklist, passed in the $blacklist parameter.
*/
function getHiddenFields( $blacklist = array() ) {
@@ -674,10 +792,10 @@ abstract class TablePager extends IndexPager {
$url = $this->getTitle()->escapeLocalURL();
$msgSubmit = wfMsgHtml( 'table_pager_limit_submit' );
return
- "<form method=\"get\" action=\"$url\">" .
- wfMsgHtml( 'table_pager_limit', $this->getLimitSelect() ) .
+ "<form method=\"get\" action=\"$url\">" .
+ wfMsgHtml( 'table_pager_limit', $this->getLimitSelect() ) .
"\n<input type=\"submit\" value=\"$msgSubmit\"/>\n" .
- $this->getHiddenFields( 'limit' ) .
+ $this->getHiddenFields( 'limit' ) .
"</form>\n";
}
@@ -691,7 +809,7 @@ abstract class TablePager extends IndexPager {
/**
* Format a table cell. The return value should be HTML, but use an empty
- * string not &nbsp; for empty cells. Do not include the <td> and </td>.
+ * string not &nbsp; for empty cells. Do not include the <td> and </td>.
*
* The current result row is available as $this->mCurrentRow, in case you
* need more context.
diff --git a/includes/PatrolLog.php b/includes/PatrolLog.php
index 35cb4a02..5f305c10 100644
--- a/includes/PatrolLog.php
+++ b/includes/PatrolLog.php
@@ -31,7 +31,7 @@ class PatrolLog {
return false;
}
}
-
+
/**
* Generate the log action text corresponding to a patrol log item
*
@@ -67,7 +67,7 @@ class PatrolLog {
return '';
}
}
-
+
/**
* Prepare log parameters for a patrolled change
*
@@ -82,6 +82,4 @@ class PatrolLog {
(int)$auto
);
}
-
}
-
diff --git a/includes/PrefixSearch.php b/includes/PrefixSearch.php
index bddfb9f1..a3ff05e2 100644
--- a/includes/PrefixSearch.php
+++ b/includes/PrefixSearch.php
@@ -5,19 +5,23 @@ class PrefixSearch {
* Do a prefix search of titles and return a list of matching page names.
* @param string $search
* @param int $limit
+ * @param array $namespaces - used if query is not explicitely prefixed
* @return array of strings
*/
- public static function titleSearch( $search, $limit ) {
+ public static function titleSearch( $search, $limit, $namespaces=array() ) {
$search = trim( $search );
if( $search == '' ) {
return array(); // Return empty result
}
-
+ $namespaces = self::validateNamespaces( $namespaces );
+
$title = Title::newFromText( $search );
if( $title && $title->getInterwiki() == '' ) {
- $ns = $title->getNamespace();
+ $ns = array($title->getNamespace());
+ if($ns[0] == NS_MAIN)
+ $ns = $namespaces; // no explicit prefix, use default namespaces
return self::searchBackend(
- $title->getNamespace(), $title->getText(), $limit );
+ $ns, $title->getText(), $limit );
}
// Is this a namespace prefix?
@@ -26,40 +30,43 @@ class PrefixSearch {
&& $title->getNamespace() != NS_MAIN
&& $title->getInterwiki() == '' ) {
return self::searchBackend(
- $title->getNamespace(), '', $limit );
+ array($title->getNamespace()), '', $limit );
}
-
- return self::searchBackend( 0, $search, $limit );
+
+ return self::searchBackend( $namespaces, $search, $limit );
}
-
-
+
+
/**
* Do a prefix search of titles and return a list of matching page names.
+ * @param array $namespaces
* @param string $search
* @param int $limit
* @return array of strings
*/
- protected static function searchBackend( $ns, $search, $limit ) {
- if( $ns == NS_MEDIA ) {
- $ns = NS_IMAGE;
- } elseif( $ns == NS_SPECIAL ) {
- return self::specialSearch( $search, $limit );
+ protected static function searchBackend( $namespaces, $search, $limit ) {
+ if( count($namespaces) == 1 ){
+ $ns = $namespaces[0];
+ if( $ns == NS_MEDIA ) {
+ $namespaces = array(NS_IMAGE);
+ } elseif( $ns == NS_SPECIAL ) {
+ return self::specialSearch( $search, $limit );
+ }
}
-
$srchres = array();
- if( wfRunHooks( 'PrefixSearchBackend', array( $ns, $search, $limit, &$srchres ) ) ) {
- return self::defaultSearchBackend( $ns, $search, $limit );
+ if( wfRunHooks( 'PrefixSearchBackend', array( $namespaces, $search, $limit, &$srchres ) ) ) {
+ return self::defaultSearchBackend( $namespaces, $search, $limit );
}
return $srchres;
}
-
+
/**
* Prefix search special-case for Special: namespace.
*/
protected static function specialSearch( $search, $limit ) {
global $wgContLang;
$searchKey = $wgContLang->caseFold( $search );
-
+
// Unlike SpecialPage itself, we want the canonical forms of both
// canonical and alias title forms...
SpecialPage::initList();
@@ -74,7 +81,7 @@ class PrefixSearch {
}
}
ksort( $keys );
-
+
$srchres = array();
foreach( $keys as $pageKey => $page ) {
if( $searchKey === '' || strpos( $pageKey, $searchKey ) === 0 ) {
@@ -86,22 +93,26 @@ class PrefixSearch {
}
return $srchres;
}
-
+
/**
* Unless overridden by PrefixSearchBackend hook...
* This is case-sensitive except the first letter (per $wgCapitalLinks)
*
- * @param int $ns Namespace to search in
+ * @param array $namespaces Namespaces to search in
* @param string $search term
* @param int $limit max number of items to return
* @return array of title strings
*/
- protected static function defaultSearchBackend( $ns, $search, $limit ) {
+ 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
// Prepare nested request
$req = new FauxRequest(array (
@@ -126,10 +137,28 @@ class PrefixSearch {
// because it does not support lists of unnamed items
$srchres[] = $pageinfo['title'];
}
-
+
return $srchres;
}
-
+
+ /**
+ * Validate an array of numerical namespace indexes
+ *
+ * @param array $namespaces
+ */
+ protected static function validateNamespaces($namespaces){
+ global $wgContLang;
+ $validNamespaces = $wgContLang->getNamespaces();
+ if( is_array($namespaces) && count($namespaces)>0 ){
+ $valid = array();
+ foreach ($namespaces as $ns){
+ if( is_numeric($ns) && array_key_exists($ns, $validNamespaces) )
+ $valid[] = $ns;
+ }
+ if( count($valid) > 0 )
+ return $valid;
+ }
+
+ return array( NS_MAIN );
+ }
}
-
-?> \ No newline at end of file
diff --git a/includes/Profiler.php b/includes/Profiler.php
index 8e1cd147..cef89dd3 100644
--- a/includes/Profiler.php
+++ b/includes/Profiler.php
@@ -1,31 +1,47 @@
<?php
/**
+ * @defgroup Profiler Profiler
+ *
+ * @file
+ * @ingroup Profiler
* This file is only included if profiling is enabled
*/
+/** backward compatibility */
$wgProfiling = true;
/**
+ * Begin profiling of a function
* @param $functioname name of the function we will profile
*/
-function wfProfileIn($functionname) {
+function wfProfileIn( $functionname ) {
global $wgProfiler;
- $wgProfiler->profileIn($functionname);
+ $wgProfiler->profileIn( $functionname );
}
/**
+ * Stop profiling of a function
* @param $functioname name of the function we have profiled
*/
-function wfProfileOut($functionname = 'missing') {
+function wfProfileOut( $functionname = 'missing' ) {
global $wgProfiler;
- $wgProfiler->profileOut($functionname);
+ $wgProfiler->profileOut( $functionname );
}
-function wfGetProfilingOutput($start, $elapsed) {
+/**
+ * Returns a profiling output to be stored in debug file
+ *
+ * @param float $start
+ * @param float $elapsed time elapsed since the beginning of the request
+ */
+function wfGetProfilingOutput( $start, $elapsed ) {
global $wgProfiler;
- return $wgProfiler->getOutput($start, $elapsed);
+ return $wgProfiler->getOutput( $start, $elapsed );
}
+/**
+ * Close opened profiling sections
+ */
function wfProfileClose() {
global $wgProfiler;
$wgProfiler->close();
@@ -39,8 +55,8 @@ if (!function_exists('memory_get_usage')) {
}
/**
+ * @ingroup Profiler
* @todo document
- * @addtogroup Profiler
*/
class Profiler {
var $mStack = array (), $mWorkStack = array (), $mCollated = array ();
@@ -57,38 +73,48 @@ class Profiler {
}
}
- function profileIn($functionname) {
+ /**
+ * Called by wfProfieIn()
+ * @param $functionname string
+ */
+ function profileIn( $functionname ) {
global $wgDebugFunctionEntry;
- if ($wgDebugFunctionEntry && function_exists('wfDebug')) {
- wfDebug(str_repeat(' ', count($this->mWorkStack)).'Entering '.$functionname."\n");
+
+ if( $wgDebugFunctionEntry ){
+ $this->debug( str_repeat( ' ', count( $this->mWorkStack ) ) . 'Entering ' . $functionname . "\n" );
}
- $this->mWorkStack[] = array($functionname, count( $this->mWorkStack ), $this->getTime(), memory_get_usage());
+
+ $this->mWorkStack[] = array( $functionname, count( $this->mWorkStack ), $this->getTime(), memory_get_usage() );
}
+ /**
+ * Called by wfProfieOut()
+ * @param $functionname string
+ */
function profileOut($functionname) {
+ global $wgDebugFunctionEntry;
+
$memory = memory_get_usage();
$time = $this->getTime();
- global $wgDebugFunctionEntry;
-
- if ($wgDebugFunctionEntry && function_exists('wfDebug')) {
- wfDebug(str_repeat(' ', count($this->mWorkStack) - 1).'Exiting '.$functionname."\n");
+ if( $wgDebugFunctionEntry ){
+ $this->debug( str_repeat( ' ', count( $this->mWorkStack ) - 1 ) . 'Exiting ' . $functionname . "\n" );
}
$bit = array_pop($this->mWorkStack);
if (!$bit) {
- wfDebug("Profiling error, !\$bit: $functionname\n");
+ $this->debug("Profiling error, !\$bit: $functionname\n");
} else {
- //if ($wgDebugProfiling) {
- if ($functionname == 'close') {
+ //if( $wgDebugProfiling ){
+ if( $functionname == 'close' ){
$message = "Profile section ended by close(): {$bit[0]}";
- wfDebug( "$message\n" );
+ $this->debug( "$message\n" );
$this->mStack[] = array( $message, 0, '0 0', 0, '0 0', 0 );
}
- elseif ($bit[0] != $functionname) {
+ elseif( $bit[0] != $functionname ){
$message = "Profiling error: in({$bit[0]}), out($functionname)";
- wfDebug( "$message\n" );
+ $this->debug( "$message\n" );
$this->mStack[] = array( $message, 0, '0 0', 0, '0 0', 0 );
}
//}
@@ -98,70 +124,86 @@ class Profiler {
}
}
+ /**
+ * called by wfProfileClose()
+ */
function close() {
- while (count($this->mWorkStack)) {
- $this->profileOut('close');
+ while( count( $this->mWorkStack ) ){
+ $this->profileOut( 'close' );
}
}
+ /**
+ * called by wfGetProfilingOutput()
+ */
function getOutput() {
- global $wgDebugFunctionEntry;
+ global $wgDebugFunctionEntry, $wgProfileCallTree;
$wgDebugFunctionEntry = false;
- if (!count($this->mStack) && !count($this->mCollated)) {
+ if( !count( $this->mStack ) && !count( $this->mCollated ) ){
return "No profiling output\n";
}
$this->close();
- global $wgProfileCallTree;
- if ($wgProfileCallTree) {
+ if( $wgProfileCallTree ){
return $this->getCallTree();
} else {
return $this->getFunctionReport();
}
}
- function getCallTree($start = 0) {
- return implode('', array_map(array (& $this, 'getCallTreeLine'), $this->remapCallTree($this->mStack)));
+ /**
+ * returns a tree of function call instead of a list of functions
+ */
+ function getCallTree() {
+ return implode( '', array_map( array( &$this, 'getCallTreeLine' ), $this->remapCallTree( $this->mStack ) ) );
}
- function remapCallTree($stack) {
- if (count($stack) < 2) {
+ /**
+ * Recursive function the format the current profiling array into a tree
+ *
+ * @param $stack profiling array
+ */
+ function remapCallTree( $stack ) {
+ if( count( $stack ) < 2 ){
return $stack;
}
$outputs = array ();
- for ($max = count($stack) - 1; $max > 0;) {
+ for( $max = count( $stack ) - 1; $max > 0; ){
/* Find all items under this entry */
$level = $stack[$max][1];
$working = array ();
- for ($i = $max -1; $i >= 0; $i --) {
- if ($stack[$i][1] > $level) {
+ for( $i = $max -1; $i >= 0; $i-- ){
+ if( $stack[$i][1] > $level ){
$working[] = $stack[$i];
} else {
break;
}
}
- $working = $this->remapCallTree(array_reverse($working));
- $output = array ();
- foreach ($working as $item) {
- array_push($output, $item);
+ $working = $this->remapCallTree( array_reverse( $working ) );
+ $output = array();
+ foreach( $working as $item ){
+ array_push( $output, $item );
}
- array_unshift($output, $stack[$max]);
+ array_unshift( $output, $stack[$max] );
$max = $i;
- array_unshift($outputs, $output);
+ array_unshift( $outputs, $output );
}
- $final = array ();
- foreach ($outputs as $output) {
- foreach ($output as $item) {
+ $final = array();
+ foreach( $outputs as $output ){
+ foreach( $output as $item ){
$final[] = $item;
}
}
return $final;
}
+ /**
+ * Callback to get a formatted line for the call tree
+ */
function getCallTreeLine($entry) {
- list ($fname, $level, $start, /* $x */, $end) = $entry;
+ list( $fname, $level, $start, /* $x */, $end) = $entry;
$delta = $end - $start;
$space = str_repeat(' ', $level);
@@ -182,66 +224,72 @@ class Profiler {
return $ru['ru_utime.tv_sec'].' '.$ru['ru_utime.tv_usec'] / 1e6;
}
+ /**
+ * Returns a list of profiled functions.
+ * Also log it into the database if $wgProfileToDatabase is set to true.
+ */
function getFunctionReport() {
+ global $wgProfileToDatabase;
+
$width = 140;
$nameWidth = $width - 65;
$format = "%-{$nameWidth}s %6d %13.3f %13.3f %13.3f%% %9d (%13.3f -%13.3f) [%d]\n";
$titleFormat = "%-{$nameWidth}s %6s %13s %13s %13s %9s\n";
$prof = "\nProfiling data\n";
- $prof .= sprintf($titleFormat, 'Name', 'Calls', 'Total', 'Each', '%', 'Mem');
+ $prof .= sprintf( $titleFormat, 'Name', 'Calls', 'Total', 'Each', '%', 'Mem' );
$this->mCollated = array ();
$this->mCalls = array ();
$this->mMemory = array ();
# Estimate profiling overhead
$profileCount = count($this->mStack);
- wfProfileIn('-overhead-total');
- for ($i = 0; $i < $profileCount; $i ++) {
- wfProfileIn('-overhead-internal');
- wfProfileOut('-overhead-internal');
+ wfProfileIn( '-overhead-total' );
+ for( $i = 0; $i < $profileCount; $i ++ ){
+ wfProfileIn( '-overhead-internal' );
+ wfProfileOut( '-overhead-internal' );
}
- wfProfileOut('-overhead-total');
+ wfProfileOut( '-overhead-total' );
# First, subtract the overhead!
- foreach ($this->mStack as $entry) {
+ foreach( $this->mStack as $entry ){
$fname = $entry[0];
$start = $entry[2];
$end = $entry[4];
$elapsed = $end - $start;
$memory = $entry[5] - $entry[3];
- if ($fname == '-overhead-total') {
+ if( $fname == '-overhead-total' ){
$overheadTotal[] = $elapsed;
$overheadMemory[] = $memory;
}
- elseif ($fname == '-overhead-internal') {
+ elseif( $fname == '-overhead-internal' ){
$overheadInternal[] = $elapsed;
}
}
- $overheadTotal = array_sum($overheadTotal) / count($overheadInternal);
- $overheadMemory = array_sum($overheadMemory) / count($overheadInternal);
- $overheadInternal = array_sum($overheadInternal) / count($overheadInternal);
+ $overheadTotal = array_sum( $overheadTotal ) / count( $overheadInternal );
+ $overheadMemory = array_sum( $overheadMemory ) / count( $overheadInternal );
+ $overheadInternal = array_sum( $overheadInternal ) / count( $overheadInternal );
# Collate
- foreach ($this->mStack as $index => $entry) {
+ foreach( $this->mStack as $index => $entry ){
$fname = $entry[0];
$start = $entry[2];
$end = $entry[4];
$elapsed = $end - $start;
$memory = $entry[5] - $entry[3];
- $subcalls = $this->calltreeCount($this->mStack, $index);
+ $subcalls = $this->calltreeCount( $this->mStack, $index );
- if (!preg_match('/^-overhead/', $fname)) {
+ if( !preg_match( '/^-overhead/', $fname ) ){
# Adjust for profiling overhead (except special values with elapsed=0
- if ( $elapsed ) {
+ if( $elapsed ) {
$elapsed -= $overheadInternal;
$elapsed -= ($subcalls * $overheadTotal);
$memory -= ($subcalls * $overheadMemory);
}
}
- if (!array_key_exists($fname, $this->mCollated)) {
+ if( !array_key_exists( $fname, $this->mCollated ) ){
$this->mCollated[$fname] = 0;
$this->mCalls[$fname] = 0;
$this->mMemory[$fname] = 0;
@@ -258,20 +306,19 @@ class Profiler {
$this->mOverhead[$fname] += $subcalls;
}
- $total = @ $this->mCollated['-total'];
+ $total = @$this->mCollated['-total'];
$this->mCalls['-overhead-total'] = $profileCount;
# Output
- arsort($this->mCollated, SORT_NUMERIC);
- foreach ($this->mCollated as $fname => $elapsed) {
+ arsort( $this->mCollated, SORT_NUMERIC );
+ foreach( $this->mCollated as $fname => $elapsed ){
$calls = $this->mCalls[$fname];
$percent = $total ? 100. * $elapsed / $total : 0;
$memory = $this->mMemory[$fname];
$prof .= sprintf($format, substr($fname, 0, $nameWidth), $calls, (float) ($elapsed * 1000), (float) ($elapsed * 1000) / $calls, $percent, $memory, ($this->mMin[$fname] * 1000.0), ($this->mMax[$fname] * 1000.0), $this->mOverhead[$fname]);
- global $wgProfileToDatabase;
- if ($wgProfileToDatabase) {
- Profiler :: logToDB($fname, (float) ($elapsed * 1000), $calls);
+ if( $wgProfileToDatabase ){
+ self::logToDB($fname, (float) ($elapsed * 1000), $calls, (float) ($memory) );
}
}
$prof .= "\nTotal: $total\n\n";
@@ -298,39 +345,54 @@ class Profiler {
}
/**
- * @static
+ * Log a function into the database.
+ *
+ * @param $name string: function name
+ * @param $timeSum float
+ * @param $eventCount int: number of times that function was called
*/
- function logToDB($name, $timeSum, $eventCount) {
+ static function logToDB( $name, $timeSum, $eventCount, $memorySum ){
# 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;
- $fname = 'Profiler::logToDB';
- $dbw = wfGetDB(DB_MASTER);
- if (!is_object($dbw))
+ $dbw = wfGetDB( DB_MASTER );
+ if( !is_object( $dbw ) )
return false;
$errorState = $dbw->ignoreErrors( true );
- $profiling = $dbw->tableName('profiling');
$name = substr($name, 0, 255);
- $encname = $dbw->strencode($name);
-
- if ($wgProfilePerHost) {
+
+ if( $wgProfilePerHost ){
$pfhost = $wguname['nodename'];
} else {
$pfhost = '';
}
-
- $sql = "UPDATE $profiling "."SET pf_count=pf_count+{$eventCount}, "."pf_time=pf_time + {$timeSum} ".
- "WHERE pf_name='{$encname}' AND pf_server='{$pfhost}'";
- $dbw->query($sql);
+
+ // Kludge
+ $timeSum = ($timeSum >= 0) ? $timeSum : 0;
+ $memorySum = ($memorySum >= 0) ? $memorySum : 0;
+
+ $dbw->update( 'profiling',
+ array(
+ "pf_count=pf_count+{$eventCount}",
+ "pf_time=pf_time+{$timeSum}",
+ "pf_memory=pf_memory+{$memorySum}",
+ ),
+ array(
+ 'pf_name' => $name,
+ 'pf_server' => $pfhost,
+ ),
+ __METHOD__ );
+
$rc = $dbw->affectedRows();
if ($rc == 0) {
$dbw->insert('profiling', array ('pf_name' => $name, 'pf_count' => $eventCount,
- 'pf_time' => $timeSum, 'pf_server' => $pfhost ), $fname, array ('IGNORE'));
+ 'pf_time' => $timeSum, 'pf_memory' => $memorySum, 'pf_server' => $pfhost ),
+ __METHOD__, array ('IGNORE'));
}
// When we upgrade to mysql 4.1, the insert+update
// can be merged into just a insert with this construct added:
@@ -344,10 +406,14 @@ class Profiler {
* Get the function name of the current profiling section
*/
function getCurrentSection() {
- $elt = end($this->mWorkStack);
+ $elt = end( $this->mWorkStack );
return $elt[0];
}
-
+
+ /**
+ * Get function caller
+ * @param $level int
+ */
static function getCaller( $level ) {
$backtrace = wfDebugBacktrace();
if ( isset( $backtrace[$level] ) ) {
@@ -362,6 +428,13 @@ class Profiler {
return $caller;
}
+ /**
+ * Add an entry in the debug log file
+ * @param $s string to output
+ */
+ function debug( $s ) {
+ if( function_exists( 'wfDebug' ) ) {
+ wfDebug( $s );
+ }
+ }
}
-
-
diff --git a/includes/ProfilerSimple.php b/includes/ProfilerSimple.php
index 20ab99c0..349a7cac 100644
--- a/includes/ProfilerSimple.php
+++ b/includes/ProfilerSimple.php
@@ -1,18 +1,22 @@
<?php
+/**
+ * @file
+ * @ingroup Profiler
+ */
require_once(dirname(__FILE__).'/Profiler.php');
/**
* Simple profiler base class.
* @todo document methods (?)
- * @addtogroup Profiler
+ * @ingroup Profiler
*/
class ProfilerSimple extends Profiler {
var $mMinimumTime = 0;
var $mProfileID = false;
function __construct() {
- global $wgRequestTime,$wgRUstart;
+ global $wgRequestTime, $wgRUstart;
if (!empty($wgRequestTime) && !empty($wgRUstart)) {
$this->mWorkStack[] = array( '-total', 0, $wgRequestTime,$this->getCpuTime($wgRUstart));
@@ -105,7 +109,7 @@ class ProfilerSimple extends Profiler {
if ( function_exists( 'getrusage' ) ) {
if ( $ru == null )
$ru = getrusage();
- return ($ru['ru_utime.tv_sec'] + $ru['ru_stime.tv_sec'] + ($ru['ru_utime.tv_usec'] +
+ return ($ru['ru_utime.tv_sec'] + $ru['ru_stime.tv_sec'] + ($ru['ru_utime.tv_usec'] +
$ru['ru_stime.tv_usec']) * 1e-6);
} else {
return 0;
@@ -119,11 +123,4 @@ class ProfilerSimple extends Profiler {
list($a,$b)=explode(" ",$time);
return (float)($a+$b);
}
-
- function debug( $s ) {
- if (function_exists( 'wfDebug' ) ) {
- wfDebug( $s );
- }
- }
}
-
diff --git a/includes/ProfilerSimpleText.php b/includes/ProfilerSimpleText.php
new file mode 100644
index 00000000..9252e302
--- /dev/null
+++ b/includes/ProfilerSimpleText.php
@@ -0,0 +1,35 @@
+<?php
+/**
+ * @file
+ * @ingroup Profiler
+ */
+
+require_once( dirname( __FILE__ ) . '/ProfilerSimple.php' );
+
+/**
+ * The least sophisticated profiler output class possible, view your source! :)
+ *
+ * Put it to StartProfiler.php like this:
+ *
+ * require_once( dirname( __FILE__ ) . '/includes/ProfilerSimpleText.php' );
+ * $wgProfiler = new ProfilerSimpleText;
+ * $wgProfiler->visible=true;
+ *
+ * @ingroup Profiler
+ */
+class ProfilerSimpleText extends ProfilerSimple {
+ public $visible=false; /* Show as <PRE> or <!-- ? */
+
+ function getFunctionReport() {
+ if ($this->visible) print "<pre>";
+ else print "<!--\n";
+ uasort($this->mCollated,array('self','sort'));
+ array_walk($this->mCollated,array('self','format'));
+ if ($this->visible) print "</pre>\n";
+ else print "-->\n";
+ }
+
+ /* dense is good */
+ static function sort($a,$b) { return $a['real']<$b['real']; /* sort descending by time elapsed */ }
+ static function format($item,$key) { printf("%3.6f %6d - %s\n",$item['real'],$item['count'], $key); }
+}
diff --git a/includes/ProfilerSimpleUDP.php b/includes/ProfilerSimpleUDP.php
index 7d2f7e21..67ad97f6 100644
--- a/includes/ProfilerSimpleUDP.php
+++ b/includes/ProfilerSimpleUDP.php
@@ -1,17 +1,19 @@
<?php
+/**
+ * @file
+ * @ingroup Profiler
+ */
-require_once(dirname(__FILE__).'/Profiler.php');
require_once(dirname(__FILE__).'/ProfilerSimple.php');
/**
* ProfilerSimpleUDP class, that sends out messages for 'udpprofile' daemon
* (the one from mediawiki/trunk/udpprofile SVN )
- * @addtogroup Profiler
+ * @ingroup Profiler
*/
class ProfilerSimpleUDP extends ProfilerSimple {
function getFunctionReport() {
- global $wgUDPProfilerHost;
- global $wgUDPProfilerPort;
+ global $wgUDPProfilerHost, $wgUDPProfilerPort;
if ( $this->mCollated['-total']['real'] < $this->mMinimumTime ) {
# Less than minimum, ignore
@@ -37,4 +39,3 @@ class ProfilerSimpleUDP extends ProfilerSimple {
socket_sendto($sock,$packet,$plength,0x100,$wgUDPProfilerHost,$wgUDPProfilerPort);
}
}
-
diff --git a/includes/ProfilerStub.php b/includes/ProfilerStub.php
index c41845a4..100cb8df 100644
--- a/includes/ProfilerStub.php
+++ b/includes/ProfilerStub.php
@@ -1,26 +1,48 @@
<?php
+/**
+ * Stub profiling functions
+ * @file
+ * @ingroup Profiler
+ */
-# Stub profiling functions
+/** backward compatibility */
+$wgProfiling = false;
+
+/** is setproctitle function available ? */
+$haveProctitle = function_exists( 'setproctitle' );
-$haveProctitle=function_exists("setproctitle");
+/**
+ * Begin profiling of a function
+ * @param $fn string
+ */
function wfProfileIn( $fn = '' ) {
global $hackwhere, $wgDBname, $haveProctitle;
- if ($haveProctitle) {
+ if( $haveProctitle ){
$hackwhere[] = $fn;
- setproctitle($fn . " [$wgDBname]");
+ setproctitle( $fn . " [$wgDBname]" );
}
}
+
+/**
+ * Stop profiling of a function
+ * @param $fn string
+ */
function wfProfileOut( $fn = '' ) {
global $hackwhere, $wgDBname, $haveProctitle;
- if (!$haveProctitle)
+ if( !$haveProctitle )
return;
- if (count($hackwhere))
- array_pop($hackwhere);
- if (count($hackwhere))
- setproctitle($hackwhere[count($hackwhere)-1] . " [$wgDBname]");
+ if( count( $hackwhere ) )
+ array_pop( $hackwhere );
+ if( count( $hackwhere ) )
+ setproctitle( $hackwhere[count( $hackwhere )-1] . " [$wgDBname]" );
}
-function wfGetProfilingOutput( $s, $e ) {}
-function wfProfileClose() {}
-$wgProfiling = false;
+/**
+ * Does nothing, just for compatibility
+ */
+function wfGetProfilingOutput( $s, $e ) {}
+/**
+ * Does nothing, just for compatibility
+ */
+function wfProfileClose() {}
diff --git a/includes/ProtectionForm.php b/includes/ProtectionForm.php
index a5ff4f3e..e7787822 100644
--- a/includes/ProtectionForm.php
+++ b/includes/ProtectionForm.php
@@ -21,7 +21,6 @@
/**
* @todo document, briefly.
- * @addtogroup SpecialPage
*/
class ProtectionForm {
var $mRestrictions = array();
@@ -54,7 +53,8 @@ class ProtectionForm {
} else if ( strlen($this->mTitle->mRestrictionsExpiry) == 0 ) {
$this->mExpiry = '';
} else {
- $this->mExpiry = wfTimestamp( TS_RFC2822, $this->mTitle->mRestrictionsExpiry );
+ // FIXME: this format is not user friendly
+ $this->mExpiry = wfTimestamp( TS_ISO_8601, $this->mTitle->mRestrictionsExpiry );
}
}
@@ -72,12 +72,21 @@ class ProtectionForm {
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;
}
}
}
}
-
+
function execute() {
global $wgRequest, $wgOut;
if( $wgRequest->wasPosted() ) {
@@ -119,8 +128,10 @@ class ProtectionForm {
$wgOut->wrapWikiMsg( "$1\n$titles", array( 'protect-cascadeon', count($cascadeSources) ) );
}
- $wgOut->setPageTitle( wfMsg( 'confirmprotect' ) );
- $wgOut->setSubtitle( wfMsg( 'protectsub', $this->mTitle->getPrefixedText() ) );
+ $sk = $wgUser->getSkin();
+ $titleLink = $sk->makeLinkObj( $this->mTitle );
+ $wgOut->setPageTitle( wfMsg( 'protect-title', $this->mTitle->getPrefixedText() ) );
+ $wgOut->setSubtitle( wfMsg( 'protect-backlink', $titleLink ) );
# Show an appropriate message if the user isn't allowed or able to change
# the protection settings at this time
@@ -141,7 +152,7 @@ class ProtectionForm {
function save() {
global $wgRequest, $wgUser, $wgOut;
-
+
if( $this->disabled ) {
$this->show();
return false;
@@ -168,6 +179,8 @@ class ProtectionForm {
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() ) {
@@ -183,7 +196,7 @@ class ProtectionForm {
$edit_restriction = $this->mRestrictions['edit'];
- if ($this->mCascade && ($edit_restriction != 'protect') &&
+ if ($this->mCascade && ($edit_restriction != 'protect') &&
!(isset($wgGroupPermissions[$edit_restriction]['protect']) && $wgGroupPermissions[$edit_restriction]['protect'] ) )
$this->mCascade = false;
@@ -196,16 +209,21 @@ class ProtectionForm {
if( !$ok ) {
throw new FatalError( "Unknown error at restriction save time." );
}
-
+
if( $wgRequest->getCheck( 'mwProtectWatch' ) ) {
$this->mArticle->doWatch();
} elseif( $this->mTitle->userIsWatching() ) {
$this->mArticle->doUnwatch();
}
-
+
return $ok;
}
+ /**
+ * Build the input form
+ *
+ * @return $out string HTML form
+ */
function buildForm() {
global $wgUser;
@@ -214,68 +232,98 @@ class ProtectionForm {
$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 .= wfOpenElement( 'form', array(
- 'id' => 'mw-Protect-Form',
- 'action' => $this->mTitle->getLocalUrl( 'action=protect' ),
- 'method' => 'post',
- 'onsubmit' => 'protectEnable(true)' ) );
-
- $out .= wfElement( 'input', array(
- 'type' => 'hidden',
- 'name' => 'wpEditToken',
- 'value' => $wgUser->editToken() ) );
+ $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 .= "<table id='mwProtectSet'>";
- $out .= "<tbody>";
- $out .= "<tr>\n";
+ $out .= Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', null, wfMsg( 'protect-legend' ) ) .
+ Xml::openElement( 'table', array( 'id' => 'mwProtectSet' ) ) .
+ Xml::openElement( 'tbody' ) .
+ "<tr>\n";
foreach( $this->mRestrictions as $action => $required ) {
/* Not all languages have V_x <-> N_x relation */
- $out .= "<th>" . wfMsgHtml( 'restriction-' . $action ) . "</th>\n";
+ $label = Xml::element( 'label',
+ array( 'for' => "mwProtect-level-$action" ),
+ wfMsg( 'restriction-' . $action ) );
+ $out .= "<th>$label</th>";
}
- $out .= "</tr>\n";
- $out .= "<tr>\n";
+ $out .= "</tr>
+ <tr>\n";
foreach( $this->mRestrictions as $action => $selected ) {
- $out .= "<td>\n";
- $out .= $this->buildSelector( $action, $selected );
- $out .= "</td>\n";
+ $out .= "<td>" .
+ $this->buildSelector( $action, $selected ) .
+ "</td>";
}
$out .= "</tr>\n";
// JavaScript will add another row with a value-chaining checkbox
- $out .= "</tbody>\n";
- $out .= "</table>\n";
-
- $out .= "<table>\n";
- $out .= "<tbody>\n";
-
- global $wgEnableCascadingProtection;
- if( $wgEnableCascadingProtection && $this->mTitle->exists() )
- $out .= '<tr><td></td><td>' . $this->buildCascadeInput() . "</td></tr>\n";
+ $out .= Xml::closeElement( 'tbody' ) .
+ Xml::closeElement( 'table' ) .
+ Xml::openElement( 'table', array( 'id' => 'mw-protect-table2' ) ) .
+ Xml::openElement( 'tbody' );
+
+ if( $this->mTitle->exists() ) {
+ $out .= '<tr>
+ <td></td>
+ <td class="mw-input">' .
+ Xml::checkLabel( wfMsg( 'protect-cascade' ), 'mwProtect-cascade', 'mwProtect-cascade', $this->mCascade, $this->disabledAttrib ) .
+ "</td>
+ </tr>\n";
+ }
- $out .= $this->buildExpiryInput();
+ $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>';
if( !$this->disabled ) {
- $out .= "<tr><td>" . $this->buildReasonInput() . "</td></tr>\n";
- $out .= "<tr><td></td><td>" . $this->buildWatchInput() . "</td></tr>\n";
- $out .= "<tr><td></td><td>" . $this->buildSubmit() . "</td></tr>\n";
+ $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 ) ) .
+ "</td>
+ </tr>
+ <tr>
+ <td></td>
+ <td class='mw-input'>" .
+ Xml::checkLabel( wfMsg( 'watchthis' ),
+ 'mwProtectWatch', 'mwProtectWatch',
+ $this->mTitle->userIsWatching() || $wgUser->getOption( 'watchdefault' ) ) .
+ "</td>
+ </tr>
+ <tr>
+ <td></td>
+ <td class='mw-submit'>" .
+ Xml::submitButton( wfMsg( 'confirm' ), array( 'id' => 'mw-Protect-submit' ) ) .
+ "</td>
+ </tr>\n";
}
- $out .= "</tbody>\n";
- $out .= "</table>\n";
+ $out .= Xml::closeElement( 'tbody' ) .
+ Xml::closeElement( 'table' ) .
+ Xml::closeElement( 'fieldset' );
if ( !$this->disabled ) {
- $out .= "</form>\n";
- $out .= $this->buildCleanupScript();
+ $out .= Xml::closeElement( 'form' ) .
+ $this->buildCleanupScript();
}
return $out;
}
function buildSelector( $action, $selected ) {
- global $wgRestrictionLevels;
+ global $wgRestrictionLevels, $wgUser;
$id = 'mwProtect-level-' . $action;
$attribs = array(
'id' => $id,
@@ -284,11 +332,20 @@ class ProtectionForm {
'onchange' => 'protectLevelsUpdate(this)',
) + $this->disabledAttrib;
- $out = wfOpenElement( 'select', $attribs );
+ $out = Xml::openElement( 'select', $attribs );
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' ) {
+ //special case, rewrite sysop to protect and editprotected
+ if( !$wgUser->isAllowed('protect') && !$wgUser->isAllowed('editprotected') && !$this->disabled )
+ continue;
+ } else {
+ if( !$wgUser->isAllowed($key) && !$this->disabled )
+ continue;
+ }
$out .= Xml::option( $this->getOptionLabel( $key ), $key, $key == $selected );
}
- $out .= "</select>\n";
+ $out .= Xml::closeElement( 'select' );
return $out;
}
@@ -310,57 +367,11 @@ class ProtectionForm {
}
}
- function buildReasonInput() {
- $id = 'mwProtect-reason';
- return wfElement( 'label', array(
- 'id' => "$id-label",
- 'for' => $id ),
- wfMsg( 'protectcomment' ) ) .
- '</td><td>' .
- wfElement( 'input', array(
- 'size' => 60,
- 'maxlength' => 255,
- 'name' => $id,
- 'id' => $id,
- 'value' => $this->mReason ) );
- }
-
- function buildCascadeInput() {
- $id = 'mwProtect-cascade';
- $ci = wfCheckLabel( wfMsg( 'protect-cascade' ), $id, $id, $this->mCascade, $this->disabledAttrib);
- return $ci;
- }
-
- function buildExpiryInput() {
- $attribs = array( 'id' => 'expires' ) + $this->disabledAttrib;
- return '<tr>'
- . '<td><label for="expires">' . wfMsgExt( 'protectexpiry', array( 'parseinline' ) ) . '</label></td>'
- . '<td>' . Xml::input( 'mwProtect-expiry', 60, $this->mExpiry, $attribs ) . '</td>'
- . '</tr>';
- }
-
- function buildWatchInput() {
- global $wgUser;
- return Xml::checkLabel(
- wfMsg( 'watchthis' ),
- 'mwProtectWatch',
- 'mwProtectWatch',
- $this->mTitle->userIsWatching() || $wgUser->getOption( 'watchdefault' )
- );
- }
-
- function buildSubmit() {
- return wfElement( 'input', array(
- 'id' => 'mw-Protect-submit',
- 'type' => 'submit',
- 'value' => wfMsg( 'confirm' ) ) );
- }
-
function buildScript() {
global $wgStylePath, $wgStyleVersion;
- return '<script type="text/javascript" src="' .
- htmlspecialchars( $wgStylePath . "/common/protect.js?$wgStyleVersion" ) .
- '"></script>';
+ return Xml::tags( 'script', array(
+ 'type' => 'text/javascript',
+ 'src' => $wgStylePath . "/common/protect.js?$wgStyleVersion" ), '' );
}
function buildCleanupScript() {
@@ -369,12 +380,12 @@ class ProtectionForm {
$CascadeableLevels = array();
foreach( $wgRestrictionLevels as $key ) {
if ( (isset($wgGroupPermissions[$key]['protect']) && $wgGroupPermissions[$key]['protect']) || $key == 'protect' ) {
- $CascadeableLevels[]="'" . wfEscapeJsString($key) . "'";
+ $CascadeableLevels[] = "'" . Xml::escapeJsString( $key ) . "'";
}
}
$script .= "[" . implode(',',$CascadeableLevels) . "];\n";
- $script .= 'protectInitialize("mwProtectSet","' . wfEscapeJsString( wfMsg( 'protect-unchain' ) ) . '","' . count($this->mApplicableTypes) . '")';
- return '<script type="text/javascript">' . $script . '</script>';
+ $script .= 'protectInitialize("mwProtectSet","' . Xml::escapeJsString( wfMsg( 'protect-unchain' ) ) . '","' . count($this->mApplicableTypes) . '")';
+ return Xml::tags( 'script', array( 'type' => 'text/javascript' ), $script );
}
/**
@@ -383,13 +394,7 @@ class ProtectionForm {
*/
function showLogExtract( &$out ) {
# Show relevant lines from the protection log:
- $out->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'protect' ) ) . "</h2>\n" );
- $logViewer = new LogViewer(
- new LogReader(
- new FauxRequest(
- array( 'page' => $this->mTitle->getPrefixedText(),
- 'type' => 'protect' ) ) ) );
- $logViewer->showList( $out );
+ $out->addHTML( Xml::element( 'h2', null, LogPage::logName( 'protect' ) ) );
+ LogEventsList::showLogExtract( $out, 'protect', $this->mTitle->getPrefixedText() );
}
-
}
diff --git a/includes/ProxyTools.php b/includes/ProxyTools.php
index 6585de42..0f010421 100644
--- a/includes/ProxyTools.php
+++ b/includes/ProxyTools.php
@@ -1,6 +1,7 @@
<?php
/**
* Functions for dealing with proxies
+ * @file
*/
/**
@@ -12,15 +13,19 @@
function wfGetForwardedFor() {
if( function_exists( 'apache_request_headers' ) ) {
// More reliable than $_SERVER due to case and -/_ folding
- $set = apache_request_headers();
- $index = 'X-Forwarded-For';
- $index2 = 'Client-ip';
+ $set = array ();
+ foreach ( apache_request_headers() as $tempName => $tempValue ) {
+ $set[ strtoupper( $tempName ) ] = $tempValue;
+ }
+ $index = strtoupper ( 'X-Forwarded-For' );
+ $index2 = strtoupper ( 'Client-ip' );
} else {
// Subject to spoofing with headers like X_Forwarded_For
$set = $_SERVER;
$index = 'HTTP_X_FORWARDED_FOR';
$index2 = 'CLIENT-IP';
}
+
#Try a couple of headers
if( isset( $set[$index] ) ) {
return $set[$index];
@@ -39,8 +44,11 @@ function wfGetForwardedFor() {
function wfGetAgent() {
if( function_exists( 'apache_request_headers' ) ) {
// More reliable than $_SERVER due to case and -/_ folding
- $set = apache_request_headers();
- $index = 'User-Agent';
+ $set = array ();
+ foreach ( apache_request_headers() as $tempName => $tempValue ) {
+ $set[ strtoupper( $tempName ) ] = $tempValue;
+ }
+ $index = strtoupper ( 'User-Agent' );
} else {
// Subject to spoofing with headers like X_Forwarded_For
$set = $_SERVER;
@@ -83,7 +91,7 @@ function wfGetIP() {
$xff = array_reverse( $xff );
$ipchain = array_merge( $ipchain, $xff );
}
-
+
# Step through XFF list and find the last address in the list which is a trusted server
# Set $ip to the IP address given by that trusted server, unless the address is not sensible (e.g. private)
foreach ( $ipchain as $i => $curIP ) {
@@ -112,9 +120,9 @@ function wfGetIP() {
function wfIsTrustedProxy( $ip ) {
global $wgSquidServers, $wgSquidServersNoPurge;
- if ( in_array( $ip, $wgSquidServers ) ||
- in_array( $ip, $wgSquidServersNoPurge ) ||
- wfIsAOLProxy( $ip )
+ if ( in_array( $ip, $wgSquidServers ) ||
+ in_array( $ip, $wgSquidServersNoPurge ) ||
+ wfIsAOLProxy( $ip )
) {
$trusted = true;
} else {
@@ -130,7 +138,7 @@ function wfIsTrustedProxy( $ip ) {
*/
function wfProxyCheck() {
global $wgBlockOpenProxies, $wgProxyPorts, $wgProxyScriptPath;
- global $wgUseMemCached, $wgMemc, $wgProxyMemcExpiry;
+ global $wgMemc, $wgProxyMemcExpiry;
global $wgProxyKey;
if ( !$wgBlockOpenProxies ) {
@@ -140,14 +148,9 @@ function wfProxyCheck() {
$ip = wfGetIP();
# Get MemCached key
- $skip = false;
- if ( $wgUseMemCached ) {
- $mcKey = wfMemcKey( 'proxy', 'ip', $ip );
- $mcValue = $wgMemc->get( $mcKey );
- if ( $mcValue ) {
- $skip = true;
- }
- }
+ $mcKey = wfMemcKey( 'proxy', 'ip', $ip );
+ $mcValue = $wgMemc->get( $mcKey );
+ $skip = (bool)$mcValue;
# Fork the processes
if ( !$skip ) {
@@ -165,9 +168,7 @@ function wfProxyCheck() {
exec( "php $params &>/dev/null &" );
}
# Set MemCached key
- if ( $wgUseMemCached ) {
- $wgMemc->set( $mcKey, 1, $wgProxyMemcExpiry );
- }
+ $wgMemc->set( $mcKey, 1, $wgProxyMemcExpiry );
}
}
@@ -217,6 +218,7 @@ function wfIsLocallyBlockedProxy( $ip ) {
* @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',
@@ -257,7 +259,3 @@ function wfIsAOLProxy( $ip ) {
}
return false;
}
-
-
-
-
diff --git a/includes/QueryPage.php b/includes/QueryPage.php
index eb4e71bf..16dc7c04 100644
--- a/includes/QueryPage.php
+++ b/includes/QueryPage.php
@@ -1,13 +1,15 @@
<?php
/**
* Contain a class for special pages
+ * @file
+ * @ingroup SpecialPages
*/
/**
- * List of query page classes and their associated special pages,
+ * List of query page classes and their associated special pages,
* for periodic updates.
*
- * DO NOT CHANGE THIS LIST without testing that
+ * DO NOT CHANGE THIS LIST without testing that
* maintenance/updateSpecialPages.php still works.
*/
global $wgQueryPages; // not redundant
@@ -29,7 +31,6 @@ $wgQueryPages = array(
array( 'MostlinkedPage', 'Mostlinked' ),
array( 'MostrevisionsPage', 'Mostrevisions' ),
array( 'FewestrevisionsPage', 'Fewestrevisions' ),
- array( 'NewPagesPage', 'Newpages' ),
array( 'ShortPagesPage', 'Shortpages' ),
array( 'UncategorizedCategoriesPage', 'Uncategorizedcategories' ),
array( 'UncategorizedPagesPage', 'Uncategorizedpages' ),
@@ -54,7 +55,7 @@ if ( !$wgDisableCounters )
* This is a class for doing query pages; since they're almost all the same,
* we factor out some of the functionality into a superclass, and let
* subclasses derive from it.
- * @addtogroup SpecialPage
+ * @ingroup SpecialPage
*/
class QueryPage {
/**
@@ -236,7 +237,7 @@ class QueryPage {
if ( isset( $row->value ) ) {
$value = $row->value;
} else {
- $value = '';
+ $value = 0;
}
$insertSql .= '(' .
@@ -305,7 +306,7 @@ class QueryPage {
# Fetch the timestamp of this update
$tRes = $dbr->select( 'querycache_info', array( 'qci_timestamp' ), array( 'qci_type' => $type ), $fname );
$tRow = $dbr->fetchObject( $tRes );
-
+
if( $tRow ) {
$updated = $wgLang->timeAndDate( $tRow->qci_timestamp, true, true );
$wgOut->addMeta( 'Data-Cache-Time', $tRow->qci_timestamp );
@@ -314,14 +315,14 @@ class QueryPage {
} else {
$wgOut->addWikiMsg( 'perfcached' );
}
-
+
# If updates on this page have been disabled, let the user know
# that the data set won't be refreshed for now
global $wgDisableQueryPageUpdate;
if( is_array( $wgDisableQueryPageUpdate ) && in_array( $this->getName(), $wgDisableQueryPageUpdate ) ) {
$wgOut->addWikiMsg( 'querypage-no-updates' );
}
-
+
}
}
@@ -334,7 +335,7 @@ class QueryPage {
$this->preprocessResults( $dbr, $res );
$wgOut->addHtml( XML::openElement( 'div', array('class' => 'mw-spcontent') ) );
-
+
# Top header and navigation
if( $shownavigation ) {
$wgOut->addHtml( $this->getPageHeader() );
@@ -352,7 +353,7 @@ class QueryPage {
return;
}
}
-
+
# The actual results; specialist subclasses will want to handle this
# with more than a straight list, so we hand them the info, plus
# an OutputPage, and let them get on with it
@@ -369,10 +370,10 @@ class QueryPage {
}
$wgOut->addHtml( XML::closeElement( 'div' ) );
-
+
return $num;
}
-
+
/**
* Format and output report results using the given information plus
* OutputPage
@@ -386,12 +387,12 @@ class QueryPage {
*/
protected function outputResults( $out, $skin, $dbr, $res, $num, $offset ) {
global $wgContLang;
-
+
if( $num > 0 ) {
$html = array();
if( !$this->listoutput )
$html[] = $this->openList( $offset );
-
+
# $res might contain the whole 1,000 rows, so we read up to
# $num [should update this to use a Pager]
for( $i = 0; $i < $num && $row = $dbr->fetchObject( $res ); $i++ ) {
@@ -405,7 +406,7 @@ class QueryPage {
: "<li{$attr}>{$line}</li>\n";
}
}
-
+
# Flush the final result
if( $this->tryLastResult() ) {
$row = null;
@@ -419,22 +420,22 @@ class QueryPage {
: "<li{$attr}>{$line}</li>\n";
}
}
-
+
if( !$this->listoutput )
$html[] = $this->closeList();
-
+
$html = $this->listoutput
? $wgContLang->listToText( $html )
: implode( '', $html );
-
+
$out->addHtml( $html );
}
}
-
+
function openList( $offset ) {
return "\n<ol start='" . ( $offset + 1 ) . "' class='special'>\n";
}
-
+
function closeList() {
return "</ol>\n";
}
@@ -448,7 +449,18 @@ class QueryPage {
* Similar to above, but packaging in a syndicated feed instead of a web page
*/
function doFeed( $class = '', $limit = 50 ) {
- global $wgFeedClasses;
+ global $wgFeed, $wgFeedClasses;
+
+ if ( !$wgFeed ) {
+ global $wgOut;
+ $wgOut->addWikiMsg( 'feed-unavailable' );
+ return;
+ }
+
+ global $wgFeedLimit;
+ if( $limit > $wgFeedLimit ) {
+ $limit = $wgFeedLimit;
+ }
if( isset($wgFeedClasses[$class]) ) {
$feed = new $wgFeedClasses[$class](
@@ -527,5 +539,3 @@ class QueryPage {
return $title->getFullURL();
}
}
-
-
diff --git a/includes/RawPage.php b/includes/RawPage.php
index 909c300b..b1e2539a 100644
--- a/includes/RawPage.php
+++ b/includes/RawPage.php
@@ -7,6 +7,7 @@
* License: GPL (http://www.gnu.org/copyleft/gpl.html)
*
* @author Gabriel Wicke <wicke@wikidev.net>
+ * @file
*/
/**
@@ -35,7 +36,7 @@ class RawPage {
$ctype = $this->mRequest->getVal( 'ctype' );
$smaxage = $this->mRequest->getIntOrNull( 'smaxage', $wgSquidMaxage );
$maxage = $this->mRequest->getInt( 'maxage', $wgSquidMaxage );
-
+
$this->mExpandTemplates = $this->mRequest->getVal( 'templates' ) === 'expand';
$this->mUseMessageCache = $this->mRequest->getBool( 'usemsgcache' );
@@ -66,7 +67,7 @@ class RawPage {
break;
}
$this->mOldId = $oldid;
-
+
# special case for 'generated' raw things: user css/js
$gen = $this->mRequest->getVal( 'gen' );
@@ -82,7 +83,7 @@ class RawPage {
$this->mGen = false;
}
$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)) {
$this->mSmaxage = intval($wgForcedRawSMaxage);
@@ -90,8 +91,8 @@ class RawPage {
$this->mSmaxage = intval( $smaxage );
}
$this->mMaxage = $maxage;
-
- # Output may contain user-specific data;
+
+ # 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 ) ||
@@ -99,7 +100,7 @@ class RawPage {
} else {
$this->mPrivateCache = false;
}
-
+
if ( $ctype == '' or ! in_array( $ctype, $allowedCTypes ) ) {
$this->mContentType = 'text/x-wiki';
} else {
@@ -125,7 +126,7 @@ class RawPage {
} else {
$url = $_SERVER['PHP_SELF'];
}
-
+
if( strcmp( $wgScript, $url ) ) {
# Internet Explorer will ignore the Content-Type header if it
# thinks it sees a file extension it recognizes. Make sure that
@@ -210,7 +211,7 @@ class RawPage {
# have the pages.
header( "HTTP/1.0 404 Not Found" );
}
-
+
// Special-case for empty CSS/JS
//
// Internet Explorer for Mac handles empty files badly;
@@ -224,7 +225,7 @@ class RawPage {
$this->mContentType == 'text/javascript' ) ) {
return "/* Empty */";
}
-
+
return $this->parseArticleText( $text );
}
@@ -239,4 +240,3 @@ class RawPage {
return $text;
}
}
-
diff --git a/includes/RecentChange.php b/includes/RecentChange.php
index 750404a9..4daf6f87 100644
--- a/includes/RecentChange.php
+++ b/includes/RecentChange.php
@@ -1,7 +1,4 @@
<?php
-/**
- *
- */
/**
* Utility class for creating new RC entries
@@ -25,6 +22,11 @@
* rc_patrolled boolean whether or not someone has marked this edit as patrolled
* rc_old_len integer byte length of the text before the edit
* rc_new_len the same after the edit
+ * rc_deleted partial deletion
+ * rc_logid the log_id value for this log entry (or zero)
+ * rc_log_type the log type (or null)
+ * rc_log_action the log action (or null)
+ * rc_params log params
*
* mExtra:
* prefixedDBkey prefixed db key, used by external app via msg queue
@@ -54,15 +56,15 @@ class RecentChange
return $rc;
}
- public static function newFromCurRow( $row, $rc_this_oldid = 0 )
+ public static function newFromCurRow( $row )
{
$rc = new RecentChange;
- $rc->loadFromCurRow( $row, $rc_this_oldid );
+ $rc->loadFromCurRow( $row );
$rc->notificationtimestamp = false;
$rc->numberofWatchingusers = false;
return $rc;
}
-
+
/**
* Obtain the recent change with a given rc_id value
*
@@ -80,7 +82,7 @@ class RecentChange
return NULL;
}
}
-
+
/**
* Find the first recent change matching some specific conditions
*
@@ -138,7 +140,8 @@ class RecentChange
# Writes the data in this object to the database
function save()
{
- global $wgLocalInterwiki, $wgPutIPinRC, $wgRC2UDPAddress, $wgRC2UDPPort, $wgRC2UDPPrefix;
+ global $wgLocalInterwiki, $wgPutIPinRC, $wgRC2UDPAddress,
+ $wgRC2UDPPort, $wgRC2UDPPrefix, $wgRC2UDPOmitBots;
$fname = 'RecentChange::save';
$dbw = wfGetDB( DB_MASTER );
@@ -207,7 +210,7 @@ class RecentChange
}
# Notify external application via UDP
- if ( $wgRC2UDPAddress ) {
+ if ( $wgRC2UDPAddress && ( !$this->mAttribs['rc_bot'] || !$wgRC2UDPOmitBots ) ) {
$conn = socket_create( AF_INET, SOCK_DGRAM, SOL_UDP );
if ( $conn ) {
$line = $wgRC2UDPPrefix . $this->getIRCLine();
@@ -217,13 +220,21 @@ class RecentChange
}
# E-mail notifications
- global $wgUseEnotif;
- if( $wgUseEnotif ) {
- # this would be better as an extension hook
- global $wgUser;
- $enotif = new EmailNotification;
+ global $wgUseEnotif, $wgShowUpdatedMarker, $wgUser;
+ if( $wgUseEnotif || $wgShowUpdatedMarker ) {
+ // Users
+ if( $this->mAttribs['rc_user'] ) {
+ $editor = ($wgUser->getId() == $this->mAttribs['rc_user']) ?
+ $wgUser : User::newFromID( $this->mAttribs['rc_user'] );
+ // Anons
+ } else {
+ $editor = ($wgUser->getName() == $this->mAttribs['rc_user_text']) ?
+ $wgUser : User::newFromName( $this->mAttribs['rc_user_text'], false );
+ }
+ # FIXME: this would be better as an extension hook
+ $enotif = new EmailNotification();
$title = Title::makeTitle( $this->mAttribs['rc_namespace'], $this->mAttribs['rc_title'] );
- $enotif->notifyOnPageChange( $wgUser, $title,
+ $enotif->notifyOnPageChange( $editor, $title,
$this->mAttribs['rc_timestamp'],
$this->mAttribs['rc_comment'],
$this->mAttribs['rc_minor'],
@@ -238,6 +249,7 @@ class RecentChange
* Mark a given change as patrolled
*
* @param mixed $change RecentChange or corresponding rc_id
+ * @returns integer number of affected rows
*/
public static function markPatrolled( $change ) {
$rcid = $change instanceof RecentChange
@@ -254,6 +266,7 @@ class RecentChange
),
__METHOD__
);
+ return $dbw->affectedRows();
}
# Makes an entry in the database corresponding to an edit
@@ -277,7 +290,7 @@ class RecentChange
'rc_type' => RC_EDIT,
'rc_minor' => $minor ? 1 : 0,
'rc_cur_id' => $title->getArticleID(),
- 'rc_user' => $user->getID(),
+ 'rc_user' => $user->getId(),
'rc_user_text' => $user->getName(),
'rc_comment' => $comment,
'rc_this_oldid' => $newId,
@@ -289,7 +302,12 @@ class RecentChange
'rc_patrolled' => 0,
'rc_new' => 0, # obsolete
'rc_old_len' => $oldSize,
- 'rc_new_len' => $newSize
+ 'rc_new_len' => $newSize,
+ 'rc_deleted' => 0,
+ 'rc_logid' => 0,
+ 'rc_log_type' => null,
+ 'rc_log_action' => '',
+ 'rc_params' => ''
);
$rc->mExtra = array(
@@ -326,7 +344,7 @@ class RecentChange
'rc_type' => RC_NEW,
'rc_minor' => $minor ? 1 : 0,
'rc_cur_id' => $title->getArticleID(),
- 'rc_user' => $user->getID(),
+ 'rc_user' => $user->getId(),
'rc_user_text' => $user->getName(),
'rc_comment' => $comment,
'rc_this_oldid' => $newId,
@@ -336,9 +354,14 @@ class RecentChange
'rc_moved_to_title' => '',
'rc_ip' => $ip,
'rc_patrolled' => 0,
- 'rc_new' => 1, # obsolete
+ 'rc_new' => 1, # obsolete
'rc_old_len' => 0,
- 'rc_new_len' => $size
+ 'rc_new_len' => $size,
+ 'rc_deleted' => 0,
+ 'rc_logid' => 0,
+ 'rc_log_type' => null,
+ 'rc_log_action' => '',
+ 'rc_params' => ''
);
$rc->mExtra = array(
@@ -372,7 +395,7 @@ class RecentChange
'rc_type' => $overRedir ? RC_MOVE_OVER_REDIRECT : RC_MOVE,
'rc_minor' => 0,
'rc_cur_id' => $oldTitle->getArticleID(),
- 'rc_user' => $user->getID(),
+ 'rc_user' => $user->getId(),
'rc_user_text' => $user->getName(),
'rc_comment' => $comment,
'rc_this_oldid' => 0,
@@ -385,6 +408,11 @@ class RecentChange
'rc_patrolled' => 1,
'rc_old_len' => NULL,
'rc_new_len' => NULL,
+ 'rc_deleted' => 0,
+ 'rc_logid' => 0, # notifyMove not used anymore
+ 'rc_log_type' => null,
+ 'rc_log_action' => '',
+ 'rc_params' => ''
);
$rc->mExtra = array(
@@ -403,10 +431,9 @@ 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, $comment, $ip='',
- $type, $action, $target, $logComment, $params )
+ # 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;
@@ -421,14 +448,14 @@ class RecentChange
$rc->mAttribs = array(
'rc_timestamp' => $timestamp,
'rc_cur_time' => $timestamp,
- 'rc_namespace' => $title->getNamespace(),
- 'rc_title' => $title->getDBkey(),
+ 'rc_namespace' => $target->getNamespace(),
+ 'rc_title' => $target->getDBkey(),
'rc_type' => RC_LOG,
'rc_minor' => 0,
- 'rc_cur_id' => $title->getArticleID(),
- 'rc_user' => $user->getID(),
+ 'rc_cur_id' => $target->getArticleID(),
+ 'rc_user' => $user->getId(),
'rc_user_text' => $user->getName(),
- 'rc_comment' => $comment,
+ 'rc_comment' => $logComment,
'rc_this_oldid' => 0,
'rc_last_oldid' => 0,
'rc_bot' => $user->isAllowed( 'bot' ) ? $wgRequest->getBool( 'bot' , true ) : 0,
@@ -439,15 +466,16 @@ class RecentChange
'rc_new' => 0, # obsolete
'rc_old_len' => NULL,
'rc_new_len' => NULL,
+ 'rc_deleted' => 0,
+ 'rc_logid' => $newId,
+ 'rc_log_type' => $type,
+ 'rc_log_action' => $action,
+ 'rc_params' => $params
);
$rc->mExtra = array(
'prefixedDBkey' => $title->getPrefixedDBkey(),
'lastTimestamp' => 0,
- 'logType' => $type,
- 'logAction' => $action,
- 'logComment' => $logComment,
- 'logTarget' => $target,
- 'logParams' => $params
+ 'actionComment' => $actionComment, // the comment appended to the action, passed from LogPage
);
$rc->save();
}
@@ -485,6 +513,12 @@ class RecentChange
'rc_new' => $row->page_is_new, # obsolete
'rc_old_len' => $row->rc_old_len,
'rc_new_len' => $row->rc_new_len,
+ 'rc_params' => isset($row->rc_params) ? $row->rc_params : '',
+ '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,
);
$this->mExtra = array();
@@ -532,18 +566,13 @@ class RecentChange
extract($this->mAttribs);
extract($this->mExtra);
- $titleObj =& $this->getTitle();
if ( $rc_type == RC_LOG ) {
- $title = Namespace::getCanonicalName( $titleObj->getNamespace() ) . $titleObj->getText();
+ $titleObj = Title::newFromText( "Log/$rc_log_type", NS_SPECIAL );
} else {
- $title = $titleObj->getPrefixedText();
+ $titleObj =& $this->getTitle();
}
- $title = $this->cleanupForIRC( $title );
-
- $bad = array("\n", "\r");
- $empty = array("", "");
$title = $titleObj->getPrefixedText();
- $title = str_replace($bad, $empty, $title);
+ $title = $this->cleanupForIRC( $title );
// FIXME: *HACK* these should be getFullURL(), hacked for SSL madness --brion 2005-12-26
if ( $rc_type == RC_LOG ) {
@@ -573,12 +602,12 @@ class RecentChange
$user = $this->cleanupForIRC( $rc_user_text );
if ( $rc_type == RC_LOG ) {
- $logTargetText = $logTarget->getPrefixedText();
- $comment = $this->cleanupForIRC( str_replace( $logTargetText, "\00302$logTargetText\00310", $rc_comment ) );
- $flag = $logAction;
+ $logTargetText = $this->getTitle()->getPrefixedText();
+ $comment = $this->cleanupForIRC( str_replace($logTargetText,"\00302$logTargetText\00310",$actionComment) );
+ $flag = $rc_log_action;
} else {
$comment = $this->cleanupForIRC( $rc_comment );
- $flag = ($rc_minor ? "M" : "") . ($rc_new ? "N" : "");
+ $flag = ($rc_new ? "N" : "") . ($rc_minor ? "M" : "") . ($rc_bot ? "B" : "");
}
# see http://www.irssi.org/documentation/formats for some colour codes. prefix is \003,
# no colour (\003) switches back to the term default
@@ -620,5 +649,3 @@ class RecentChange
}
}
}
-
-
diff --git a/includes/RefreshLinksJob.php b/includes/RefreshLinksJob.php
index 367d994f..f95e5a50 100644
--- a/includes/RefreshLinksJob.php
+++ b/includes/RefreshLinksJob.php
@@ -2,6 +2,8 @@
/**
* Background job to update links for a given title.
+ *
+ * @ingroup JobQueue
*/
class RefreshLinksJob extends Job {
@@ -17,7 +19,7 @@ class RefreshLinksJob extends Job {
global $wgParser;
wfProfileIn( __METHOD__ );
- $linkCache =& LinkCache::singleton();
+ $linkCache = LinkCache::singleton();
$linkCache->clear();
if ( is_null( $this->title ) ) {
@@ -45,4 +47,3 @@ class RefreshLinksJob extends Job {
return true;
}
}
-
diff --git a/includes/Revision.php b/includes/Revision.php
index 05a4a68a..d0ccb46d 100644
--- a/includes/Revision.php
+++ b/includes/Revision.php
@@ -1,6 +1,7 @@
<?php
/**
* @todo document
+ * @file
*/
/**
@@ -34,10 +35,8 @@ class Revision {
* @param Title $title
* @param int $id
* @return Revision
- * @access public
- * @static
*/
- public static function newFromTitle( &$title, $id = 0 ) {
+ public static function newFromTitle( $title, $id = 0 ) {
if( $id ) {
$matchId = intval( $id );
} else {
@@ -59,7 +58,7 @@ class Revision {
* @access public
* @static
*/
- public static function loadFromId( &$db, $id ) {
+ public static function loadFromId( $db, $id ) {
return Revision::loadFromConds( $db,
array( 'page_id=rev_page',
'rev_id' => intval( $id ) ) );
@@ -99,7 +98,7 @@ class Revision {
* @access public
* @static
*/
- public static function loadFromTitle( &$db, $title, $id = 0 ) {
+ public static function loadFromTitle( $db, $title, $id = 0 ) {
if( $id ) {
$matchId = intval( $id );
} else {
@@ -125,7 +124,7 @@ class Revision {
* @access public
* @static
*/
- public static function loadFromTimestamp( &$db, &$title, $timestamp ) {
+ public static function loadFromTimestamp( $db, $title, $timestamp ) {
return Revision::loadFromConds(
$db,
array( 'rev_timestamp' => $db->timestamp( $timestamp ),
@@ -186,7 +185,7 @@ class Revision {
* @access public
* @static
*/
- public static function fetchAllRevisions( &$title ) {
+ public static function fetchAllRevisions( $title ) {
return Revision::fetchFromConds(
wfGetDB( DB_SLAVE ),
array( 'page_namespace' => $title->getNamespace(),
@@ -204,7 +203,7 @@ class Revision {
* @access public
* @static
*/
- public static function fetchRevision( &$title ) {
+ public static function fetchRevision( $title ) {
return Revision::fetchFromConds(
wfGetDB( DB_SLAVE ),
array( 'rev_id=page_latest',
@@ -225,21 +224,13 @@ class Revision {
* @static
*/
private static function fetchFromConds( $db, $conditions ) {
+ $fields = self::selectFields();
+ $fields[] = 'page_namespace';
+ $fields[] = 'page_title';
+ $fields[] = 'page_latest';
$res = $db->select(
array( 'page', 'revision' ),
- array( 'page_namespace',
- 'page_title',
- 'page_latest',
- 'rev_id',
- 'rev_page',
- 'rev_text_id',
- 'rev_comment',
- 'rev_user_text',
- 'rev_user',
- 'rev_minor_edit',
- 'rev_timestamp',
- 'rev_deleted',
- 'rev_len' ),
+ $fields,
$conditions,
'Revision::fetchRow',
array( 'LIMIT' => 1 ) );
@@ -248,21 +239,43 @@ class Revision {
}
/**
- * Return the list of revision fields that should be selected to create
+ * Return the list of revision fields that should be selected to create
* a new revision.
*/
static function selectFields() {
- return array(
+ return array(
'rev_id',
'rev_page',
'rev_text_id',
'rev_timestamp',
'rev_comment',
- 'rev_minor_edit',
- 'rev_user',
'rev_user_text,'.
+ 'rev_user',
+ 'rev_minor_edit',
'rev_deleted',
- 'rev_len'
+ 'rev_len',
+ 'rev_parent_id'
+ );
+ }
+
+ /**
+ * Return the list of text fields that should be selected to read the
+ * revision text
+ */
+ static function selectTextFields() {
+ return array(
+ 'old_text',
+ 'old_flags'
+ );
+ }
+ /**
+ * Return the list of page fields that should be selected from page table
+ */
+ static function selectPageFields() {
+ return array(
+ 'page_namespace',
+ 'page_title',
+ 'page_latest'
);
}
@@ -281,11 +294,16 @@ class Revision {
$this->mMinorEdit = intval( $row->rev_minor_edit );
$this->mTimestamp = $row->rev_timestamp;
$this->mDeleted = intval( $row->rev_deleted );
-
+
+ if( !isset( $row->rev_parent_id ) )
+ $this->mParentId = is_null($row->rev_parent_id) ? null : 0;
+ else
+ $this->mParentId = intval( $row->rev_parent_id );
+
if( !isset( $row->rev_len ) || is_null( $row->rev_len ) )
$this->mSize = null;
else
- $this->mSize = intval( $row->rev_len );
+ $this->mSize = intval( $row->rev_len );
if( isset( $row->page_latest ) ) {
$this->mCurrent = ( $row->rev_id == $row->page_latest );
@@ -317,7 +335,8 @@ class Revision {
$this->mTimestamp = isset( $row['timestamp'] ) ? strval( $row['timestamp'] ) : wfTimestamp( TS_MW );
$this->mDeleted = isset( $row['deleted'] ) ? intval( $row['deleted'] ) : 0;
$this->mSize = isset( $row['len'] ) ? intval( $row['len'] ) : null;
-
+ $this->mParentId = isset( $row['parent_id'] ) ? intval( $row['parent_id'] ) : null;
+
// Enforce spacing trimming on supplied text
$this->mComment = isset( $row['comment'] ) ? trim( strval( $row['comment'] ) ) : null;
$this->mText = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null;
@@ -338,23 +357,34 @@ class Revision {
*/
/**
+ * Get revision ID
* @return int
*/
- function getId() {
+ public function getId() {
return $this->mId;
}
/**
+ * Get text row ID
* @return int
*/
- function getTextId() {
+ public function getTextId() {
return $this->mTextId;
}
/**
+ * Get parent revision ID (the original previous page revision)
+ * @return int
+ */
+ public function getParentId() {
+ return $this->mParentId;
+ }
+
+ /**
* Returns the length of the text in this revision, or null if unknown.
+ * @return int
*/
- function getSize() {
+ public function getSize() {
return $this->mSize;
}
@@ -362,7 +392,7 @@ class Revision {
* Returns the title of the page associated with this entry.
* @return Title
*/
- function getTitle() {
+ public function getTitle() {
if( isset( $this->mTitle ) ) {
return $this->mTitle;
}
@@ -375,7 +405,7 @@ class Revision {
'Revision::getTitle' );
if( $row ) {
$this->mTitle = Title::makeTitle( $row->page_namespace,
- $row->page_title );
+ $row->page_title );
}
return $this->mTitle;
}
@@ -384,14 +414,15 @@ class Revision {
* Set the title of the revision
* @param Title $title
*/
- function setTitle( $title ) {
+ public function setTitle( $title ) {
$this->mTitle = $title;
}
/**
+ * Get the page ID
* @return int
*/
- function getPage() {
+ public function getPage() {
return $this->mPage;
}
@@ -399,7 +430,7 @@ class Revision {
* Fetch revision's user id if it's available to all users
* @return int
*/
- function getUser() {
+ public function getUser() {
if( $this->isDeleted( self::DELETED_USER ) ) {
return 0;
} else {
@@ -411,7 +442,7 @@ class Revision {
* Fetch revision's user id without regard for the current user's permissions
* @return string
*/
- function getRawUser() {
+ public function getRawUser() {
return $this->mUser;
}
@@ -419,7 +450,7 @@ class Revision {
* Fetch revision's username if it's available to all users
* @return string
*/
- function getUserText() {
+ public function getUserText() {
if( $this->isDeleted( self::DELETED_USER ) ) {
return "";
} else {
@@ -431,10 +462,10 @@ class Revision {
* Fetch revision's username without regard for view restrictions
* @return string
*/
- function getRawUserText() {
+ public function getRawUserText() {
return $this->mUserText;
}
-
+
/**
* Fetch revision comment if it's available to all users
* @return string
@@ -451,14 +482,14 @@ class Revision {
* Fetch revision comment without regard for the current user's permissions
* @return string
*/
- function getRawComment() {
+ public function getRawComment() {
return $this->mComment;
}
/**
* @return bool
*/
- function isMinor() {
+ public function isMinor() {
return (bool)$this->mMinorEdit;
}
@@ -466,7 +497,7 @@ class Revision {
* int $field one of DELETED_* bitfield constants
* @return bool
*/
- function isDeleted( $field ) {
+ public function isDeleted( $field ) {
return ($this->mDeleted & $field) == $field;
}
@@ -474,31 +505,31 @@ class Revision {
* Fetch revision text if it's available to all users
* @return string
*/
- function getText() {
+ public function getText() {
if( $this->isDeleted( self::DELETED_TEXT ) ) {
return "";
} else {
return $this->getRawText();
}
}
-
+
/**
* Fetch revision text without regard for view restrictions
* @return string
*/
- function getRawText() {
+ public function getRawText() {
if( is_null( $this->mText ) ) {
// Revision text is immutable. Load on demand:
$this->mText = $this->loadText();
}
return $this->mText;
}
-
+
/**
* Fetch revision text if it's available to THIS user
* @return string
*/
- function revText() {
+ public function revText() {
if( !$this->userCan( self::DELETED_TEXT ) ) {
return "";
} else {
@@ -509,54 +540,79 @@ class Revision {
/**
* @return string
*/
- function getTimestamp() {
+ public function getTimestamp() {
return wfTimestamp(TS_MW, $this->mTimestamp);
}
/**
* @return bool
*/
- function isCurrent() {
+ public function isCurrent() {
return $this->mCurrent;
}
/**
+ * Get previous revision for this title
* @return Revision
*/
- function getPrevious() {
- $prev = $this->mTitle->getPreviousRevisionID( $this->mId );
- if ( $prev ) {
- return Revision::newFromTitle( $this->mTitle, $prev );
- } else {
- return null;
+ public function getPrevious() {
+ if( $this->getTitle() ) {
+ $prev = $this->getTitle()->getPreviousRevisionID( $this->getId() );
+ if( $prev ) {
+ return Revision::newFromTitle( $this->getTitle(), $prev );
+ }
}
+ return null;
}
/**
* @return Revision
*/
- function getNext() {
- $next = $this->mTitle->getNextRevisionID( $this->mId );
- if ( $next ) {
- return Revision::newFromTitle( $this->mTitle, $next );
+ public function getNext() {
+ if( $this->getTitle() ) {
+ $next = $this->getTitle()->getNextRevisionID( $this->getId() );
+ if ( $next ) {
+ return Revision::newFromTitle( $this->getTitle(), $next );
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get previous revision Id for this page_id
+ * This is used to populate rev_parent_id on save
+ * @param Database $db
+ * @return int
+ */
+ private function getPreviousRevisionId( $db ) {
+ if( is_null($this->mPage) ) {
+ return 0;
+ }
+ # Use page_latest if ID is not given
+ if( !$this->mId ) {
+ $prevId = $db->selectField( 'page', 'page_latest',
+ array( 'page_id' => $this->mPage ),
+ __METHOD__ );
} else {
- return null;
+ $prevId = $db->selectField( 'revision', 'rev_id',
+ array( 'rev_page' => $this->mPage, 'rev_id < ' . $this->mId ),
+ __METHOD__,
+ array( 'ORDER BY' => 'rev_id DESC' ) );
}
+ return intval($prevId);
}
- /**#@-*/
/**
* Get revision text associated with an old or archive row
* $row is usually an object from wfFetchRow(), both the flags and the text
* field must be included
- * @static
+ *
* @param integer $row Id of a row
* @param string $prefix table prefix (default 'old_')
* @return string $text|false the text requested
*/
public static function getRevisionText( $row, $prefix = 'old_' ) {
- $fname = 'Revision::getRevisionText';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
# Get data
$textField = $prefix . 'text';
@@ -571,7 +627,7 @@ class Revision {
if( isset( $row->$textField ) ) {
$text = $row->$textField;
} else {
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return false;
}
@@ -580,7 +636,7 @@ class Revision {
$url=$text;
@list(/* $proto */,$path)=explode('://',$url,2);
if ($path=="") {
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return false;
}
$text=ExternalStore::fetchFromURL($url);
@@ -600,7 +656,7 @@ class Revision {
$obj = unserialize( $text );
if ( !is_object( $obj ) ) {
// Invalid object
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return false;
}
$text = $obj->getText();
@@ -614,7 +670,7 @@ class Revision {
$text = $wgContLang->iconv( $wgLegacyEncoding, $wgInputEncoding, $text );
}
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $text;
}
@@ -625,11 +681,10 @@ class Revision {
* data is compressed, and 'utf-8' if we're saving in UTF-8
* mode.
*
- * @static
* @param mixed $text reference to a text
* @return string
*/
- function compressRevisionText( &$text ) {
+ public static function compressRevisionText( &$text ) {
global $wgCompressRevisions;
$flags = array();
@@ -655,11 +710,10 @@ class Revision {
* @param Database $dbw
* @return int
*/
- function insertOn( &$dbw ) {
+ public function insertOn( $dbw ) {
global $wgDefaultExternalStore;
-
- $fname = 'Revision::insertOn';
- wfProfileIn( $fname );
+
+ wfProfileIn( __METHOD__ );
$data = $this->mText;
$flags = Revision::compressRevisionText( $data );
@@ -692,7 +746,7 @@ class Revision {
'old_id' => $old_id,
'old_text' => $data,
'old_flags' => $flags,
- ), $fname
+ ), __METHOD__
);
$this->mTextId = $dbw->insertId();
}
@@ -713,11 +767,15 @@ class Revision {
'rev_timestamp' => $dbw->timestamp( $this->mTimestamp ),
'rev_deleted' => $this->mDeleted,
'rev_len' => $this->mSize,
- ), $fname
+ 'rev_parent_id' => $this->mParentId ? $this->mParentId : $this->getPreviousRevisionId( $dbw )
+ ), __METHOD__
);
$this->mId = !is_null($rev_id) ? $rev_id : $dbw->insertId();
- wfProfileOut( $fname );
+
+ wfRunHooks( 'RevisionInsertComplete', array( &$this, $data, $flags ) );
+
+ wfProfileOut( __METHOD__ );
return $this->mId;
}
@@ -726,23 +784,21 @@ class Revision {
* Currently hardcoded to the 'text' table storage engine.
*
* @return string
- * @access private
*/
- function loadText() {
- $fname = 'Revision::loadText';
- wfProfileIn( $fname );
-
+ private function loadText() {
+ wfProfileIn( __METHOD__ );
+
// Caching may be beneficial for massive use of external storage
global $wgRevisionCacheExpiry, $wgMemc;
$key = wfMemcKey( 'revisiontext', 'textid', $this->getTextId() );
if( $wgRevisionCacheExpiry ) {
$text = $wgMemc->get( $key );
if( is_string( $text ) ) {
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $text;
}
}
-
+
// If we kept data for lazy extraction, use it now...
if ( isset( $this->mTextRow ) ) {
$row = $this->mTextRow;
@@ -750,14 +806,14 @@ class Revision {
} else {
$row = null;
}
-
+
if( !$row ) {
// Text data is immutable; check slaves first.
$dbr = wfGetDB( DB_SLAVE );
$row = $dbr->selectRow( 'text',
array( 'old_text', 'old_flags' ),
array( 'old_id' => $this->getTextId() ),
- $fname);
+ __METHOD__ );
}
if( !$row ) {
@@ -766,16 +822,16 @@ class Revision {
$row = $dbw->selectRow( 'text',
array( 'old_text', 'old_flags' ),
array( 'old_id' => $this->getTextId() ),
- $fname);
+ __METHOD__ );
}
- $text = Revision::getRevisionText( $row );
-
+ $text = self::getRevisionText( $row );
+
if( $wgRevisionCacheExpiry ) {
$wgMemc->set( $key, $text, $wgRevisionCacheExpiry );
}
-
- wfProfileOut( $fname );
+
+ wfProfileOut( __METHOD__ );
return $text;
}
@@ -794,7 +850,7 @@ class Revision {
* @param bool $minor
* @return Revision
*/
- public static function newNullRevision( &$dbw, $pageId, $summary, $minor ) {
+ public static function newNullRevision( $dbw, $pageId, $summary, $minor ) {
wfProfileIn( __METHOD__ );
$current = $dbw->selectRow(
@@ -812,6 +868,7 @@ class Revision {
'comment' => $summary,
'minor_edit' => $minor,
'text_id' => $current->rev_text_id,
+ 'parent_id' => $current->page_latest
) );
} else {
$revision = null;
@@ -820,7 +877,7 @@ class Revision {
wfProfileOut( __METHOD__ );
return $revision;
}
-
+
/**
* Determine if the current user is allowed to view a particular
* field of this revision, if it's marked as deleted.
@@ -829,11 +886,11 @@ class Revision {
* self::DELETED_USER
* @return bool
*/
- function userCan( $field ) {
+ public function userCan( $field ) {
if( ( $this->mDeleted & $field ) == $field ) {
global $wgUser;
$permission = ( $this->mDeleted & self::DELETED_RESTRICTED ) == self::DELETED_RESTRICTED
- ? 'hiderevision'
+ ? 'suppressrevision'
: 'deleterevision';
wfDebug( "Checking for $permission due to $field match on $this->mDeleted\n" );
return $wgUser->isAllowed( $permission );
@@ -846,20 +903,28 @@ class Revision {
/**
* Get rev_timestamp from rev_id, without loading the rest of the row
* @param integer $id
+ * @param integer $pageid, optional
*/
- static function getTimestampFromID( $id ) {
+ static function getTimestampFromId( $id, $pageId = 0 ) {
$dbr = wfGetDB( DB_SLAVE );
- $timestamp = $dbr->selectField( 'revision', 'rev_timestamp',
- array( 'rev_id' => $id ), __METHOD__ );
+ $conds = array( 'rev_id' => $id );
+ if( $pageId ) {
+ $conds['rev_page'] = $pageId;
+ }
+ $timestamp = $dbr->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
if ( $timestamp === false ) {
# Not in slave, try master
$dbw = wfGetDB( DB_MASTER );
- $timestamp = $dbw->selectField( 'revision', 'rev_timestamp',
- array( 'rev_id' => $id ), __METHOD__ );
+ $timestamp = $dbw->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
}
- return $timestamp;
+ return wfTimestamp( TS_MW, $timestamp );
}
-
+
+ /**
+ * Get count of revisions per page...not very efficient
+ * @param Database $db
+ * @param int $id, page id
+ */
static function countByPageId( $db, $id ) {
$row = $db->selectRow( 'revision', 'COUNT(*) AS revCount',
array( 'rev_page' => $id ), __METHOD__ );
@@ -868,7 +933,12 @@ class Revision {
}
return 0;
}
-
+
+ /**
+ * Get count of revisions per page...not very efficient
+ * @param Database $db
+ * @param Title $title
+ */
static function countByTitle( $db, $title ) {
$id = $title->getArticleId();
if( $id ) {
@@ -885,6 +955,3 @@ define( 'MW_REV_DELETED_TEXT', Revision::DELETED_TEXT );
define( 'MW_REV_DELETED_COMMENT', Revision::DELETED_COMMENT );
define( 'MW_REV_DELETED_USER', Revision::DELETED_USER );
define( 'MW_REV_DELETED_RESTRICTED', Revision::DELETED_RESTRICTED );
-
-
-
diff --git a/includes/Sanitizer.php b/includes/Sanitizer.php
index c1c8daf3..28b1c275 100644
--- a/includes/Sanitizer.php
+++ b/includes/Sanitizer.php
@@ -20,7 +20,8 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
- * @addtogroup Parser
+ * @file
+ * @ingroup Parser
*/
/**
@@ -327,7 +328,7 @@ $wgHtmlEntityAliases = array(
/**
* XHTML sanitizer for MediaWiki
- * @addtogroup Parser
+ * @ingroup Parser
*/
class Sanitizer {
const NONE = 0;
@@ -383,7 +384,7 @@ class Sanitizer {
$htmlelements = array_merge( $htmlsingle, $htmlpairs, $htmlnest );
# Convert them all to hashtables for faster lookup
- $vars = array( 'htmlpairs', 'htmlsingle', 'htmlsingleonly', 'htmlnest', 'tabletags',
+ $vars = array( 'htmlpairs', 'htmlsingle', 'htmlsingleonly', 'htmlnest', 'tabletags',
'htmllist', 'listtags', 'htmlsingleallowed', 'htmlelements' );
foreach ( $vars as $var ) {
$$var = array_flip( $$var );
@@ -419,7 +420,7 @@ class Sanitizer {
$optstack = array();
array_push ($optstack, $ot);
while ( ( ( $ot = @array_pop( $tagstack ) ) != $t ) &&
- isset( $htmlsingleallowed[$ot] ) )
+ isset( $htmlsingleallowed[$ot] ) )
{
array_push ($optstack, $ot);
}
@@ -582,7 +583,7 @@ class Sanitizer {
return Sanitizer::validateAttributes( $attribs,
Sanitizer::attributeWhitelist( $element ) );
}
-
+
/**
* Take an array of attribute names and values and normalize or discard
* illegal values for the given whitelist.
@@ -624,7 +625,7 @@ class Sanitizer {
}
return $out;
}
-
+
/**
* Merge two sets of HTML attributes.
* Conflicting items in the second set will override those
@@ -641,7 +642,7 @@ class Sanitizer {
if( isset( $a['class'] )
&& isset( $b['class'] )
&& $a['class'] !== $b['class'] ) {
-
+
$out['class'] = implode( ' ',
array_unique(
preg_split( '/\s+/',
@@ -651,7 +652,7 @@ class Sanitizer {
}
return $out;
}
-
+
/**
* Pick apart some CSS and check it for forbidden or unsafe structures.
* Returns a sanitized string, or false if it was just too evil.
@@ -666,7 +667,7 @@ class Sanitizer {
// Remove any comments; IE gets token splitting wrong
$stripped = StringUtils::delimiterReplace( '/*', '*/', ' ', $stripped );
-
+
$value = $stripped;
// ... and continue checks
@@ -678,7 +679,7 @@ class Sanitizer {
# haxx0r
return false;
}
-
+
return $value;
}
@@ -797,7 +798,7 @@ class Sanitizer {
$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!
@@ -920,7 +921,7 @@ class Sanitizer {
self::normalizeWhitespace(
Sanitizer::normalizeCharReferences( $text ) ) );
}
-
+
private static function normalizeWhitespace( $text ) {
return preg_replace(
'/\r\n|[\x20\x0d\x0a\x09]/',
@@ -972,8 +973,8 @@ class Sanitizer {
/**
* If the named entity is defined in the HTML 4.0/XHTML 1.0 DTD,
- * return the named entity reference as is. If the entity is a
- * MediaWiki-specific alias, returns the HTML equivalent. Otherwise,
+ * return the named entity reference as is. If the entity is a
+ * MediaWiki-specific alias, returns the HTML equivalent. Otherwise,
* returns HTML-escaped text of pseudo-entity source (eg &amp;foo;)
*
* @param string $name
@@ -1219,7 +1220,7 @@ class Sanitizer {
# 11.2.6
'td' => array_merge( $common, $tablecell, $tablealign ),
'th' => array_merge( $common, $tablecell, $tablealign ),
-
+
# 13.2
# Not usually allowed, but may be used for extension-style hooks
# such as <math> when it is rasterized
@@ -1250,7 +1251,7 @@ class Sanitizer {
'rb' => $common,
'rt' => $common, #array_merge( $common, array( 'rbspan' ) ),
'rp' => $common,
-
+
# MathML root element, where used for extensions
# 'title' may not be 100% valid here; it's XHTML
# http://www.w3.org/TR/REC-MathML/
@@ -1343,5 +1344,3 @@ class Sanitizer {
}
}
-
-
diff --git a/includes/SearchEngine.php b/includes/SearchEngine.php
index c22e58d7..edd93cce 100644
--- a/includes/SearchEngine.php
+++ b/includes/SearchEngine.php
@@ -1,7 +1,14 @@
<?php
/**
+ * @defgroup Search Search
+ *
+ * @file
+ * @ingroup Search
+ */
+
+/**
* Contain a class for special pages
- * @addtogroup Search
+ * @ingroup Search
*/
class SearchEngine {
var $limit = 10;
@@ -35,7 +42,7 @@ class SearchEngine {
function searchTitle( $term ) {
return null;
}
-
+
/**
* If an exact title match can be find, or a very slightly close match,
* return the title. If no match, returns NULL.
@@ -51,7 +58,7 @@ class SearchEngine {
if($wgContLang->hasVariants()){
$allSearchTerms = array_merge($allSearchTerms,$wgContLang->convertLinkToAllVariants($searchterm));
}
-
+
foreach($allSearchTerms as $term){
# Exact match? No need to look further.
@@ -59,34 +66,35 @@ class SearchEngine {
if (is_null($title))
return NULL;
- if ( $title->getNamespace() == NS_SPECIAL || $title->exists() ) {
+ if ( $title->getNamespace() == NS_SPECIAL || $title->isExternal()
+ || $title->exists() ) {
return $title;
}
# Now try all lower case (i.e. first letter capitalized)
#
$title = Title::newFromText( $wgContLang->lc( $term ) );
- if ( $title->exists() ) {
+ if ( $title && $title->exists() ) {
return $title;
}
# Now try capitalized string
#
$title = Title::newFromText( $wgContLang->ucwords( $term ) );
- if ( $title->exists() ) {
+ if ( $title && $title->exists() ) {
return $title;
}
# Now try all upper case
#
$title = Title::newFromText( $wgContLang->uc( $term ) );
- if ( $title->exists() ) {
+ if ( $title && $title->exists() ) {
return $title;
}
# Now try Word-Caps-Breaking-At-Word-Breaks, for hyphenated names etc
$title = Title::newFromText( $wgContLang->ucwordbreaks($term) );
- if ( $title->exists() ) {
+ if ( $title && $title->exists() ) {
return $title;
}
@@ -94,11 +102,11 @@ class SearchEngine {
if( !$wgCapitalLinks ) {
// Catch differs-by-first-letter-case-only
$title = Title::newFromText( $wgContLang->ucfirst( $term ) );
- if ( $title->exists() ) {
+ if ( $title && $title->exists() ) {
return $title;
}
$title = Title::newFromText( $wgContLang->lcfirst( $term ) );
- if ( $title->exists() ) {
+ if ( $title && $title->exists() ) {
return $title;
}
}
@@ -123,7 +131,7 @@ class SearchEngine {
if ( $title->getNamespace() == NS_USER ) {
return $title;
}
-
+
# 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.
@@ -145,7 +153,7 @@ class SearchEngine {
if( preg_match( '/^"([^"]+)"$/', $searchterm, $matches ) ) {
return SearchEngine::getNearMatch( $matches[1] );
}
-
+
return NULL;
}
@@ -178,6 +186,37 @@ class SearchEngine {
}
/**
+ * Parse some common prefixes: all (search everything)
+ * or namespace names
+ *
+ * @param string $query
+ */
+ function replacePrefixes( $query ){
+ global $wgContLang;
+
+ if( strpos($query,':') === false )
+ return $query; // nothing to do
+
+ $parsed = $query;
+ $allkeyword = wfMsgForContent('searchall').":";
+ if( strncmp($query, $allkeyword, strlen($allkeyword)) == 0 ){
+ $this->namespaces = null;
+ $parsed = substr($query,strlen($allkeyword));
+ } else if( strpos($query,':') !== false ) {
+ $prefix = substr($query,0,strpos($query,':'));
+ $index = $wgContLang->getNsIndex($prefix);
+ if($index !== false){
+ $this->namespaces = array($index);
+ $parsed = substr($query,strlen($prefix)+1);
+ }
+ }
+ if(trim($parsed) == '')
+ return $query; // prefix was the whole query
+
+ return $parsed;
+ }
+
+ /**
* Make a list of searchable namespaces and their canonical names.
* @return array
*/
@@ -191,6 +230,51 @@ class SearchEngine {
}
return $arr;
}
+
+ /**
+ * Extract default namespaces to search from the given user's
+ * settings, returning a list of index numbers.
+ *
+ * @param User $user
+ * @return array
+ * @static
+ */
+ public static function userNamespaces( &$user ) {
+ $arr = array();
+ foreach( SearchEngine::searchableNamespaces() as $ns => $name ) {
+ if( $user->getOption( 'searchNs' . $ns ) ) {
+ $arr[] = $ns;
+ }
+ }
+ return $arr;
+ }
+
+ /**
+ * Find snippet highlight settings for a given user
+ *
+ * @param User $user
+ * @return array contextlines, contextchars
+ * @static
+ */
+ public static function userHighlightPrefs( &$user ){
+ //$contextlines = $user->getOption( 'contextlines', 5 );
+ //$contextchars = $user->getOption( 'contextchars', 50 );
+ $contextlines = 2; // Hardcode this. Old defaults sucked. :)
+ $contextchars = 75; // same as above.... :P
+ return array($contextlines, $contextchars);
+ }
+
+ /**
+ * An array of namespaces indexes to be searched by default
+ *
+ * @return array
+ * @static
+ */
+ public static function defaultNamespaces(){
+ global $wgNamespacesToBeSearchedDefault;
+
+ return array_keys($wgNamespacesToBeSearchedDefault, true);
+ }
/**
* Return a 'cleaned up' search string
@@ -206,6 +290,8 @@ 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() {
@@ -213,7 +299,7 @@ class SearchEngine {
if( $wgSearchType ) {
$class = $wgSearchType;
} elseif( $wgDBtype == 'mysql' ) {
- $class = 'SearchMySQL4';
+ $class = 'SearchMySQL';
} else if ( $wgDBtype == 'postgres' ) {
$class = 'SearchPostgres';
} else if ( $wgDBtype == 'oracle' ) {
@@ -250,11 +336,41 @@ class SearchEngine {
function updateTitle( $id, $title ) {
// no-op
}
+
+ /**
+ * Get OpenSearch suggestion template
+ *
+ * @return string
+ * @static
+ */
+ public static function getOpenSearchTemplate() {
+ global $wgOpenSearchTemplate, $wgServer, $wgScriptPath;
+ if($wgOpenSearchTemplate)
+ return $wgOpenSearchTemplate;
+ else{
+ $ns = implode(',',SearchEngine::defaultNamespaces());
+ if(!$ns) $ns = "0";
+ return $wgServer . $wgScriptPath . '/api.php?action=opensearch&search={searchTerms}&namespace='.$ns;
+ }
+ }
+
+ /**
+ * Get internal MediaWiki Suggest template
+ *
+ * @return string
+ * @static
+ */
+ public static function getMWSuggestTemplate() {
+ global $wgMWSuggestTemplate, $wgServer, $wgScriptPath;
+ if($wgMWSuggestTemplate)
+ return $wgMWSuggestTemplate;
+ else
+ return $wgServer . $wgScriptPath . '/api.php?action=opensearch&search={searchTerms}&namespace={namespaces}';
+ }
}
-
/**
- * @addtogroup Search
+ * @ingroup Search
*/
class SearchResultSet {
/**
@@ -309,15 +425,47 @@ class SearchResultSet {
}
/**
- * Some search modes return a suggested alternate term if there are
- * no exact hits. Check hasSuggestion() first.
+ * @return string suggested query, null if none
+ */
+ function getSuggestionQuery(){
+ return null;
+ }
+
+ /**
+ * @return string highlighted suggested query, '' if none
+ */
+ function getSuggestionSnippet(){
+ return '';
+ }
+
+ /**
+ * Return information about how and from where the results were fetched,
+ * should be useful for diagnostics and debugging
*
* @return string
- * @access public
*/
- function getSuggestion() {
- return '';
+ function getInfo() {
+ return null;
+ }
+
+ /**
+ * Return a result set of hits on other (multiple) wikis associated with this one
+ *
+ * @return SearchResultSet
+ */
+ function getInterwikiResults() {
+ return null;
+ }
+
+ /**
+ * Check if there are results on other wikis
+ *
+ * @return boolean
+ */
+ function hasInterwikiResults() {
+ return $this->getInterwikiResults() != null;
}
+
/**
* Fetches next search result, or false.
@@ -328,7 +476,7 @@ class SearchResultSet {
function next() {
return false;
}
-
+
/**
* Frees the result set, if applicable.
* @ access public
@@ -340,7 +488,7 @@ class SearchResultSet {
/**
- * @addtogroup Search
+ * @ingroup Search
*/
class SearchResultTooMany {
## Some search engines may bail out if too many matches are found
@@ -348,12 +496,42 @@ class SearchResultTooMany {
/**
- * @addtogroup Search
+ * @fixme This class is horribly factored. It would probably be better to have
+ * a useful base class to which you pass some standard information, then let
+ * the fancy self-highlighters extend that.
+ * @ingroup Search
*/
class SearchResult {
+ var $mRevision = null;
function SearchResult( $row ) {
$this->mTitle = Title::makeTitle( $row->page_namespace, $row->page_title );
+ if( !is_null($this->mTitle) )
+ $this->mRevision = Revision::newFromTitle( $this->mTitle );
+ }
+
+ /**
+ * Check if this is result points to an invalid title
+ *
+ * @return boolean
+ * @access public
+ */
+ function isBrokenTitle(){
+ if( is_null($this->mTitle) )
+ return true;
+ return false;
+ }
+
+ /**
+ * Check if target page is missing, happens when index is out of date
+ *
+ * @return boolean
+ * @access public
+ */
+ function isMissingRevision(){
+ if( !$this->mRevision )
+ return true;
+ return false;
}
/**
@@ -370,20 +548,607 @@ class SearchResult {
function getScore() {
return null;
}
+
+ /**
+ * Lazy initialization of article text from DB
+ */
+ protected function initText(){
+ if( !isset($this->mText) ){
+ $this->mText = $this->mRevision->getText();
+ }
+ }
+
+ /**
+ * @param array $terms terms to highlight
+ * @return string highlighted text snippet, null (and not '') if not supported
+ */
+ function getTextSnippet($terms){
+ global $wgUser, $wgAdvancedSearchHighlighting;
+ $this->initText();
+ list($contextlines,$contextchars) = SearchEngine::userHighlightPrefs($wgUser);
+ $h = new SearchHighlighter();
+ if( $wgAdvancedSearchHighlighting )
+ return $h->highlightText( $this->mText, $terms, $contextlines, $contextchars );
+ else
+ return $h->highlightSimple( $this->mText, $terms, $contextlines, $contextchars );
+ }
+
+ /**
+ * @param array $terms terms to highlight
+ * @return string highlighted title, '' if not supported
+ */
+ function getTitleSnippet($terms){
+ return '';
+ }
+
+ /**
+ * @param array $terms terms to highlight
+ * @return string highlighted redirect name (redirect to this page), '' if none or not supported
+ */
+ function getRedirectSnippet($terms){
+ return '';
+ }
+
+ /**
+ * @return Title object for the redirect to this page, null if none or not supported
+ */
+ function getRedirectTitle(){
+ return null;
+ }
+
+ /**
+ * @return string highlighted relevant section name, null if none or not supported
+ */
+ function getSectionSnippet(){
+ return '';
+ }
+
+ /**
+ * @return Title object (pagename+fragment) for the section, null if none or not supported
+ */
+ function getSectionTitle(){
+ return null;
+ }
+
+ /**
+ * @return string timestamp
+ */
+ function getTimestamp(){
+ return $this->mRevision->getTimestamp();
+ }
+
+ /**
+ * @return int number of words
+ */
+ function getWordCount(){
+ $this->initText();
+ return str_word_count( $this->mText );
+ }
+
+ /**
+ * @return int size in bytes
+ */
+ function getByteSize(){
+ $this->initText();
+ return strlen( $this->mText );
+ }
+
+ /**
+ * @return boolean if hit has related articles
+ */
+ function hasRelated(){
+ return false;
+ }
+
+ /**
+ * @return interwiki prefix of the title (return iw even if title is broken)
+ */
+ function getInterwikiPrefix(){
+ return '';
+ }
}
/**
- * @addtogroup Search
+ * Highlight bits of wikitext
+ *
+ * @ingroup Search
*/
-class SearchEngineDummy {
- function search( $term ) {
- return null;
+class SearchHighlighter {
+ var $mCleanWikitext = true;
+
+ function SearchHighlighter($cleanupWikitext = true){
+ $this->mCleanWikitext = $cleanupWikitext;
+ }
+
+ /**
+ * Default implementation of wikitext highlighting
+ *
+ * @param string $text
+ * @param array $terms Terms to highlight (unescaped)
+ * @param int $contextlines
+ * @param int $contextchars
+ * @return string
+ */
+ public function highlightText( $text, $terms, $contextlines, $contextchars ) {
+ global $wgLang, $wgContLang;
+ global $wgSearchHighlightBoundaries;
+ $fname = __METHOD__;
+
+ if($text == '')
+ return '';
+
+ // spli text into text + templates/links/tables
+ $spat = "/(\\{\\{)|(\\[\\[[^\\]:]+:)|(\n\\{\\|)";
+ // first capture group is for detecting nested templates/links/tables/references
+ $endPatterns = array(
+ 1 => '/(\{\{)|(\}\})/', // template
+ 2 => '/(\[\[)|(\]\])/', // image
+ 3 => "/(\n\\{\\|)|(\n\\|\\})/"); // table
+
+ // FIXME: this should prolly be a hook or something
+ if(function_exists('wfCite')){
+ $spat .= '|(<ref>)'; // references via cite extension
+ $endPatterns[4] = '/(<ref>)|(<\/ref>)/';
+ }
+ $spat .= '/';
+ $textExt = array(); // text extracts
+ $otherExt = array(); // other extracts
+ wfProfileIn( "$fname-split" );
+ $start = 0;
+ $textLen = strlen($text);
+ $count = 0; // sequence number to maintain ordering
+ while( $start < $textLen ){
+ // find start of template/image/table
+ if( preg_match( $spat, $text, $matches, PREG_OFFSET_CAPTURE, $start ) ){
+ $epat = '';
+ foreach($matches as $key => $val){
+ if($key > 0 && $val[1] != -1){
+ if($key == 2){
+ // see if this is an image link
+ $ns = substr($val[0],2,-1);
+ if( $wgContLang->getNsIndex($ns) != NS_IMAGE )
+ break;
+
+ }
+ $epat = $endPatterns[$key];
+ $this->splitAndAdd( $textExt, $count, substr( $text, $start, $val[1] - $start ) );
+ $start = $val[1];
+ break;
+ }
+ }
+ if( $epat ){
+ // find end (and detect any nested elements)
+ $level = 0;
+ $offset = $start + 1;
+ $found = false;
+ while( preg_match( $epat, $text, $endMatches, PREG_OFFSET_CAPTURE, $offset ) ){
+ if( array_key_exists(2,$endMatches) ){
+ // found end
+ if($level == 0){
+ $len = strlen($endMatches[2][0]);
+ $off = $endMatches[2][1];
+ $this->splitAndAdd( $otherExt, $count,
+ substr( $text, $start, $off + $len - $start ) );
+ $start = $off + $len;
+ $found = true;
+ break;
+ } else{
+ // end of nested element
+ $level -= 1;
+ }
+ } else{
+ // nested
+ $level += 1;
+ }
+ $offset = $endMatches[0][1] + strlen($endMatches[0][0]);
+ }
+ if( ! $found ){
+ // couldn't find appropriate closing tag, skip
+ $this->splitAndAdd( $textExt, $count, substr( $text, $start, strlen($matches[0][0]) ) );
+ $start += strlen($matches[0][0]);
+ }
+ continue;
+ }
+ }
+ // else: add as text extract
+ $this->splitAndAdd( $textExt, $count, substr($text,$start) );
+ break;
+ }
+
+ $all = $textExt + $otherExt; // these have disjunct key sets
+
+ wfProfileOut( "$fname-split" );
+
+ // 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]);
+ }
+
+
+ }
+ $anyterm = implode( '|', $terms );
+ $phrase = implode("$wgSearchHighlightBoundaries+", $terms );
+
+ // FIXME: a hack to scale contextchars, a correct solution
+ // would be to have contextchars actually be char and not byte
+ // length, and do proper utf-8 substrings and lengths everywhere,
+ // but PHP is making that very hard and unclean to implement :(
+ $scale = strlen($anyterm) / mb_strlen($anyterm);
+ $contextchars = intval( $contextchars * $scale );
+
+ $patPre = "(^|$wgSearchHighlightBoundaries)";
+ $patPost = "($wgSearchHighlightBoundaries|$)";
+
+ $pat1 = "/(".$phrase.")/ui";
+ $pat2 = "/$patPre(".$anyterm.")$patPost/ui";
+
+ wfProfileIn( "$fname-extract" );
+
+ $left = $contextlines;
+
+ $snippets = array();
+ $offsets = array();
+
+ // show beginning only if it contains all words
+ $first = 0;
+ $firstText = '';
+ foreach($textExt as $index => $line){
+ if(strlen($line)>0 && $line[0] != ';' && $line[0] != ':'){
+ $firstText = $this->extract( $line, 0, $contextchars * $contextlines );
+ $first = $index;
+ break;
+ }
+ }
+ if( $firstText ){
+ $succ = true;
+ // check if first text contains all terms
+ foreach($terms as $term){
+ if( ! preg_match("/$patPre".$term."$patPost/ui", $firstText) ){
+ $succ = false;
+ break;
+ }
+ }
+ if( $succ ){
+ $snippets[$first] = $firstText;
+ $offsets[$first] = 0;
+ }
+ }
+ if( ! $snippets ) {
+ // match whole query on text
+ $this->process($pat1, $textExt, $left, $contextchars, $snippets, $offsets);
+ // match whole query on templates/tables/images
+ $this->process($pat1, $otherExt, $left, $contextchars, $snippets, $offsets);
+ // match any words on text
+ $this->process($pat2, $textExt, $left, $contextchars, $snippets, $offsets);
+ // match any words on templates/tables/images
+ $this->process($pat2, $otherExt, $left, $contextchars, $snippets, $offsets);
+
+ ksort($snippets);
+ }
+
+ // add extra chars to each snippet to make snippets constant size
+ $extended = array();
+ if( count( $snippets ) == 0){
+ // couldn't find the target words, just show beginning of article
+ $targetchars = $contextchars * $contextlines;
+ $snippets[$first] = '';
+ $offsets[$first] = 0;
+ } else{
+ // if begin of the article contains the whole phrase, show only that !!
+ if( array_key_exists($first,$snippets) && preg_match($pat1,$snippets[$first])
+ && $offsets[$first] < $contextchars * 2 ){
+ $snippets = array ($first => $snippets[$first]);
+ }
+
+ // calc by how much to extend existing snippets
+ $targetchars = intval( ($contextchars * $contextlines) / count ( $snippets ) );
+ }
+
+ foreach($snippets as $index => $line){
+ $extended[$index] = $line;
+ $len = strlen($line);
+ if( $len < $targetchars - 20 ){
+ // complete this line
+ if($len < strlen( $all[$index] )){
+ $extended[$index] = $this->extract( $all[$index], $offsets[$index], $offsets[$index]+$targetchars, $offsets[$index]);
+ $len = strlen( $extended[$index] );
+ }
+
+ // add more lines
+ $add = $index + 1;
+ while( $len < $targetchars - 20
+ && array_key_exists($add,$all)
+ && !array_key_exists($add,$snippets) ){
+ $offsets[$add] = 0;
+ $tt = "\n".$this->extract( $all[$add], 0, $targetchars - $len, $offsets[$add] );
+ $extended[$add] = $tt;
+ $len += strlen( $tt );
+ $add++;
+ }
+ }
+ }
+
+ //$snippets = array_map('htmlspecialchars', $extended);
+ $snippets = $extended;
+ $last = -1;
+ $extract = '';
+ foreach($snippets as $index => $line){
+ if($last == -1)
+ $extract .= $line; // first line
+ elseif($last+1 == $index && $offsets[$last]+strlen($snippets[$last]) >= strlen($all[$last]))
+ $extract .= " ".$line; // continous lines
+ else
+ $extract .= '<b> ... </b>' . $line;
+
+ $last = $index;
+ }
+ if( $extract )
+ $extract .= '<b> ... </b>';
+
+ $processed = array();
+ foreach($terms as $term){
+ if( ! isset($processed[$term]) ){
+ $pat3 = "/$patPre(".$term.")$patPost/ui"; // highlight word
+ $extract = preg_replace( $pat3,
+ "\\1<span class='searchmatch'>\\2</span>\\3", $extract );
+ $processed[$term] = true;
+ }
+ }
+
+ wfProfileOut( "$fname-extract" );
+
+ return $extract;
+ }
+
+ /**
+ * Split text into lines and add it to extracts array
+ *
+ * @param array $extracts index -> $line
+ * @param int $count
+ * @param string $text
+ */
+ function splitAndAdd(&$extracts, &$count, $text){
+ $split = explode( "\n", $this->mCleanWikitext? $this->removeWiki($text) : $text );
+ foreach($split as $line){
+ $tt = trim($line);
+ if( $tt )
+ $extracts[$count++] = $tt;
+ }
+ }
+
+ /**
+ * Do manual case conversion for non-ascii chars
+ *
+ * @param unknown_type $matches
+ */
+ function caseCallback($matches){
+ global $wgContLang;
+ if( strlen($matches[0]) > 1 ){
+ return '['.$wgContLang->lc($matches[0]).$wgContLang->uc($matches[0]).']';
+ } else
+ return $matches[0];
+ }
+
+ /**
+ * Extract part of the text from start to end, but by
+ * not chopping up words
+ * @param string $text
+ * @param int $start
+ * @param int $end
+ * @param int $posStart (out) actual start position
+ * @param int $posEnd (out) actual end position
+ * @return string
+ */
+ function extract($text, $start, $end, &$posStart = null, &$posEnd = null ){
+ global $wgContLang;
+
+ if( $start != 0)
+ $start = $this->position( $text, $start, 1 );
+ if( $end >= strlen($text) )
+ $end = strlen($text);
+ else
+ $end = $this->position( $text, $end );
+
+ if(!is_null($posStart))
+ $posStart = $start;
+ if(!is_null($posEnd))
+ $posEnd = $end;
+
+ if($end > $start)
+ return substr($text, $start, $end-$start);
+ else
+ return '';
+ }
+
+ /**
+ * Find a nonletter near a point (index) in the text
+ *
+ * @param string $text
+ * @param int $point
+ * @param int $offset to found index
+ * @return int nearest nonletter index, or beginning of utf8 char if none
+ */
+ function position($text, $point, $offset=0 ){
+ $tolerance = 10;
+ $s = max( 0, $point - $tolerance );
+ $l = min( strlen($text), $point + $tolerance ) - $s;
+ $m = array();
+ if( preg_match('/[ ,.!?~!@#$%^&*\(\)+=\-\\\|\[\]"\'<>]/', substr($text,$s,$l), $m, PREG_OFFSET_CAPTURE ) ){
+ return $m[0][1] + $s + $offset;
+ } else{
+ // check if point is on a valid first UTF8 char
+ $char = ord( $text[$point] );
+ while( $char >= 0x80 && $char < 0xc0 ) {
+ // skip trailing bytes
+ $point++;
+ if($point >= strlen($text))
+ return strlen($text);
+ $char = ord( $text[$point] );
+ }
+ return $point;
+
+ }
+ }
+
+ /**
+ * Search extracts for a pattern, and return snippets
+ *
+ * @param string $pattern regexp for matching lines
+ * @param array $extracts extracts to search
+ * @param int $linesleft number of extracts to make
+ * @param int $contextchars length of snippet
+ * @param array $out map for highlighted snippets
+ * @param array $offsets map of starting points of snippets
+ * @protected
+ */
+ function process( $pattern, $extracts, &$linesleft, &$contextchars, &$out, &$offsets ){
+ if($linesleft == 0)
+ return; // nothing to do
+ foreach($extracts as $index => $line){
+ if( array_key_exists($index,$out) )
+ continue; // this line already highlighted
+
+ $m = array();
+ if ( !preg_match( $pattern, $line, $m, PREG_OFFSET_CAPTURE ) )
+ continue;
+
+ $offset = $m[0][1];
+ $len = strlen($m[0][0]);
+ if($offset + $len < $contextchars)
+ $begin = 0;
+ elseif( $len > $contextchars)
+ $begin = $offset;
+ else
+ $begin = $offset + intval( ($len - $contextchars) / 2 );
+
+ $end = $begin + $contextchars;
+
+ $posBegin = $begin;
+ // basic snippet from this line
+ $out[$index] = $this->extract($line,$begin,$end,$posBegin);
+ $offsets[$index] = $posBegin;
+ $linesleft--;
+ if($linesleft == 0)
+ return;
+ }
+ }
+
+ /**
+ * Basic wikitext removal
+ * @protected
+ */
+ function removeWiki($text) {
+ $fname = __METHOD__;
+ wfProfileIn( $fname );
+
+ //$text = preg_replace("/'{2,5}/", "", $text);
+ //$text = preg_replace("/\[[a-z]+:\/\/[^ ]+ ([^]]+)\]/", "\\2", $text);
+ //$text = preg_replace("/\[\[([^]|]+)\]\]/", "\\1", $text);
+ //$text = preg_replace("/\[\[([^]]+\|)?([^|]]+)\]\]/", "\\2", $text);
+ //$text = preg_replace("/\\{\\|(.*?)\\|\\}/", "", $text);
+ //$text = preg_replace("/\\[\\[[A-Za-z_-]+:([^|]+?)\\]\\]/", "", $text);
+ $text = preg_replace("/\\{\\{([^|]+?)\\}\\}/", "", $text);
+ $text = preg_replace("/\\{\\{([^|]+\\|)(.*?)\\}\\}/", "\\2", $text);
+ $text = preg_replace("/\\[\\[([^|]+?)\\]\\]/", "\\1", $text);
+ $text = preg_replace_callback("/\\[\\[([^|]+\\|)(.*?)\\]\\]/", array($this,'linkReplace'), $text);
+ //$text = preg_replace("/\\[\\[([^|]+\\|)(.*?)\\]\\]/", "\\2", $text);
+ $text = preg_replace("/<\/?[^>]+>/", "", $text);
+ $text = preg_replace("/'''''/", "", $text);
+ $text = preg_replace("/('''|<\/?[iIuUbB]>)/", "", $text);
+ $text = preg_replace("/''/", "", $text);
+
+ wfProfileOut( $fname );
+ return $text;
+ }
+
+ /**
+ * callback to replace [[target|caption]] kind of links, if
+ * the target is category or image, leave it
+ *
+ * @param array $matches
+ */
+ function linkReplace($matches){
+ $colon = strpos( $matches[1], ':' );
+ if( $colon === false )
+ return $matches[2]; // replace with caption
+ global $wgContLang;
+ $ns = substr( $matches[1], 0, $colon );
+ $index = $wgContLang->getNsIndex($ns);
+ if( $index !== false && ($index == NS_IMAGE || $index == NS_CATEGORY) )
+ return $matches[0]; // return the whole thing
+ else
+ return $matches[2];
+
}
- function setLimitOffset($l, $o) {}
- function legalSearchChars() {}
- function update() {}
- function setnamespaces() {}
- function searchtitle() {}
- function searchtext() {}
+
+ /**
+ * Simple & fast snippet extraction, but gives completely unrelevant
+ * snippets
+ *
+ * @param string $text
+ * @param array $terms
+ * @param int $contextlines
+ * @param int $contextchars
+ * @return string
+ */
+ 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";
+
+ $lineno = 0;
+
+ $extract = "";
+ wfProfileIn( "$fname-extract" );
+ foreach ( $lines as $line ) {
+ if ( 0 == $contextlines ) {
+ break;
+ }
+ ++$lineno;
+ $m = array();
+ if ( ! preg_match( $pat1, $line, $m ) ) {
+ continue;
+ }
+ --$contextlines;
+ $pre = $wgContLang->truncate( $m[1], -$contextchars, ' ... ' );
+
+ if ( count( $m ) < 3 ) {
+ $post = '';
+ } else {
+ $post = $wgContLang->truncate( $m[3], $contextchars, ' ... ' );
+ }
+
+ $found = $m[2];
+
+ $line = htmlspecialchars( $pre . $found . $post );
+ $pat2 = '/(' . $terms . ")/i";
+ $line = preg_replace( $pat2,
+ "<span class='searchmatch'>\\1</span>", $line );
+
+ $extract .= "${line}\n";
+ }
+ wfProfileOut( "$fname-extract" );
+
+ return $extract;
+ }
+
}
+/**
+ * Dummy class to be used when non-supported Database engine is present.
+ * @fixme Dummy class should probably try something at least mildly useful,
+ * such as a LIKE search through titles.
+ * @ingroup Search
+ */
+class SearchEngineDummy extends SearchEngine {
+ // no-op
+}
diff --git a/includes/SearchMySQL.php b/includes/SearchMySQL.php
index 905075ef..f9b71c8e 100644
--- a/includes/SearchMySQL.php
+++ b/includes/SearchMySQL.php
@@ -18,11 +18,64 @@
# http://www.gnu.org/copyleft/gpl.html
/**
- * Search engine hook base class for MySQL.
- * Specific bits for MySQL 3 and 4 variants are in child classes.
- * @addtogroup Search
+ * @file
+ * @ingroup Search
+ */
+
+/**
+ * Search engine hook for MySQL 4+
+ * @ingroup Search
*/
class SearchMySQL extends SearchEngine {
+ var $strictMatching = true;
+
+ /** @todo document */
+ function __construct( $db ) {
+ $this->db = $db;
+ }
+
+ /** @todo document */
+ function parseQuery( $filteredText, $fulltext ) {
+ global $wgContLang;
+ $lc = SearchEngine::legalSearchChars(); // Minus format chars
+ $searchon = '';
+ $this->searchTerms = array();
+
+ # FIXME: This doesn't handle parenthetical expressions.
+ $m = array();
+ if( preg_match_all( '/([-+<>~]?)(([' . $lc . ']+)(\*?)|"[^"]*")/',
+ $filteredText, $m, PREG_SET_ORDER ) ) {
+ foreach( $m as $terms ) {
+ if( $searchon !== '' ) $searchon .= ' ';
+ if( $this->strictMatching && ($terms[1] == '') ) {
+ $terms[1] = '+';
+ }
+ $searchon .= $terms[1] . $wgContLang->stripForSearch( $terms[2] );
+ if( !empty( $terms[3] ) ) {
+ // Match individual terms in result highlighting...
+ $regexp = preg_quote( $terms[3], '/' );
+ if( $terms[4] ) $regexp .= "[0-9A-Za-z_]+";
+ } else {
+ // Match the quoted term in result highlighting...
+ $regexp = preg_quote( str_replace( '"', '', $terms[2] ), '/' );
+ }
+ $this->searchTerms[] = $regexp;
+ }
+ wfDebug( "Would search with '$searchon'\n" );
+ wfDebug( 'Match with /' . implode( '|', $this->searchTerms ) . "/\n" );
+ } else {
+ wfDebug( "Can't understand search query '{$filteredText}'\n" );
+ }
+
+ $searchon = $this->db->strencode( $searchon );
+ $field = $this->getIndexField( $fulltext );
+ return " MATCH($field) AGAINST('$searchon' IN BOOLEAN MODE) ";
+ }
+
+ public static function legalSearchChars() {
+ return "\"*" . parent::legalSearchChars();
+ }
+
/**
* Perform a full text search query and return a result set.
*
@@ -67,6 +120,8 @@ class SearchMySQL extends SearchEngine {
* @private
*/
function queryNamespaces() {
+ if( is_null($this->namespaces) )
+ return ''; # search all
$namespaces = implode( ',', $this->namespaces );
if ($namespaces == '') {
$namespaces = '0';
@@ -154,7 +209,7 @@ class SearchMySQL extends SearchEngine {
'si_page' => $id,
'si_title' => $title,
'si_text' => $text
- ), 'SearchMySQL4::update' );
+ ), __METHOD__ );
}
/**
@@ -170,13 +225,13 @@ class SearchMySQL extends SearchEngine {
$dbw->update( 'searchindex',
array( 'si_title' => $title ),
array( 'si_page' => $id ),
- 'SearchMySQL4::updateTitle',
+ __METHOD__,
array( $dbw->lowPriorityOption() ) );
}
}
/**
- * @addtogroup Search
+ * @ingroup Search
*/
class MySQLSearchResultSet extends SearchResultSet {
function MySQLSearchResultSet( $resultSet, $terms ) {
@@ -200,10 +255,8 @@ class MySQLSearchResultSet extends SearchResultSet {
return new SearchResult( $row );
}
}
-
+
function free() {
$this->mResultSet->free();
}
}
-
-
diff --git a/includes/SearchMySQL4.php b/includes/SearchMySQL4.php
index 271dbe1d..3e2bb2d1 100644
--- a/includes/SearchMySQL4.php
+++ b/includes/SearchMySQL4.php
@@ -18,57 +18,17 @@
# http://www.gnu.org/copyleft/gpl.html
/**
+ * @file
+ * @ingroup Search
+ */
+
+/**
* Search engine hook for MySQL 4+
- * @addtogroup Search
+ * This class retained for backwards compatibility...
+ * The meat's been moved to SearchMySQL, since the 3.x variety is gone.
+ * @ingroup Search
+ * @deprecated
*/
class SearchMySQL4 extends SearchMySQL {
- var $strictMatching = true;
-
- /** @todo document */
- function SearchMySQL4( $db ) {
- $this->db = $db;
- }
-
- /** @todo document */
- function parseQuery( $filteredText, $fulltext ) {
- global $wgContLang;
- $lc = SearchEngine::legalSearchChars(); // Minus format chars
- $searchon = '';
- $this->searchTerms = array();
-
- # FIXME: This doesn't handle parenthetical expressions.
- $m = array();
- if( preg_match_all( '/([-+<>~]?)(([' . $lc . ']+)(\*?)|"[^"]*")/',
- $filteredText, $m, PREG_SET_ORDER ) ) {
- foreach( $m as $terms ) {
- if( $searchon !== '' ) $searchon .= ' ';
- if( $this->strictMatching && ($terms[1] == '') ) {
- $terms[1] = '+';
- }
- $searchon .= $terms[1] . $wgContLang->stripForSearch( $terms[2] );
- if( !empty( $terms[3] ) ) {
- // Match individual terms in result highlighting...
- $regexp = preg_quote( $terms[3], '/' );
- if( $terms[4] ) $regexp .= "[0-9A-Za-z_]+";
- } else {
- // Match the quoted term in result highlighting...
- $regexp = preg_quote( str_replace( '"', '', $terms[2] ), '/' );
- }
- $this->searchTerms[] = "\b$regexp\b";
- }
- wfDebug( "Would search with '$searchon'\n" );
- wfDebug( 'Match with /\b' . implode( '\b|\b', $this->searchTerms ) . "\b/\n" );
- } else {
- wfDebug( "Can't understand search query '{$filteredText}'\n" );
- }
-
- $searchon = $this->db->strencode( $searchon );
- $field = $this->getIndexField( $fulltext );
- return " MATCH($field) AGAINST('$searchon' IN BOOLEAN MODE) ";
- }
-
- public static function legalSearchChars() {
- return "\"*" . parent::legalSearchChars();
- }
+ /* whee */
}
-
diff --git a/includes/SearchOracle.php b/includes/SearchOracle.php
index 95c59288..bf9368d1 100644
--- a/includes/SearchOracle.php
+++ b/includes/SearchOracle.php
@@ -18,8 +18,13 @@
# http://www.gnu.org/copyleft/gpl.html
/**
+ * @file
+ * @ingroup Search
+ */
+
+/**
* Search engine hook base class for Oracle (ConText).
- * @addtogroup Search
+ * @ingroup Search
*/
class SearchOracle extends SearchEngine {
function __construct($db) {
@@ -70,6 +75,8 @@ class SearchOracle extends SearchEngine {
* @private
*/
function queryNamespaces() {
+ if( is_null($this->namespaces) )
+ return '';
$namespaces = implode(',', $this->namespaces);
if ($namespaces == '') {
$namespaces = '0';
@@ -208,7 +215,7 @@ class SearchOracle extends SearchEngine {
}
/**
- * @addtogroup Search
+ * @ingroup Search
*/
class OracleSearchResultSet extends SearchResultSet {
function __construct($resultSet, $terms) {
@@ -231,5 +238,3 @@ class OracleSearchResultSet extends SearchResultSet {
return new SearchResult($row);
}
}
-
-
diff --git a/includes/SearchPostgres.php b/includes/SearchPostgres.php
index 59110a5a..88e4a0da 100644
--- a/includes/SearchPostgres.php
+++ b/includes/SearchPostgres.php
@@ -18,18 +18,23 @@
# http://www.gnu.org/copyleft/gpl.html
/**
+ * @file
+ * @ingroup Search
+ */
+
+/**
* Search engine hook base class for Postgres
- * @addtogroup Search
+ * @ingroup Search
*/
class SearchPostgres extends SearchEngine {
- function SearchPostgres( $db ) {
+ function __construct( $db ) {
$this->db = $db;
}
/**
* Perform a full text search query via tsearch2 and return a result set.
- * Currently searches a page's current title (page.page_title) and
+ * Currently searches a page's current title (page.page_title) and
* latest revision article text (pagecontent.old_text)
*
* @param string $term - Raw search term
@@ -174,11 +179,13 @@ class SearchPostgres extends SearchEngine {
$query .= ' AND page_is_redirect = 0';
## Namespaces - defaults to 0
- if ( count($this->namespaces) < 1)
- $query .= ' AND page_namespace = 0';
- else {
- $namespaces = implode( ',', $this->namespaces );
- $query .= " AND page_namespace IN ($namespaces)";
+ if( !is_null($this->namespaces) ){ // null -> search all
+ if ( count($this->namespaces) < 1)
+ $query .= ' AND page_namespace = 0';
+ else {
+ $namespaces = implode( ',', $this->namespaces );
+ $query .= " AND page_namespace IN ($namespaces)";
+ }
}
$query .= " ORDER BY score DESC, page_id DESC";
@@ -208,11 +215,11 @@ class SearchPostgres extends SearchEngine {
} ## end of the SearchPostgres class
/**
- * @addtogroup Search
+ * @ingroup Search
*/
class PostgresSearchResult extends SearchResult {
- function PostgresSearchResult( $row ) {
- $this->mTitle = Title::makeTitle( $row->page_namespace, $row->page_title );
+ function __construct( $row ) {
+ parent::__construct($row);
$this->score = $row->score;
}
function getScore() {
@@ -221,10 +228,10 @@ class PostgresSearchResult extends SearchResult {
}
/**
- * @addtogroup Search
+ * @ingroup Search
*/
class PostgresSearchResultSet extends SearchResultSet {
- function PostgresSearchResultSet( $resultSet, $terms ) {
+ function __construct( $resultSet, $terms ) {
$this->mResultSet = $resultSet;
$this->mTerms = $terms;
}
@@ -246,6 +253,3 @@ class PostgresSearchResultSet extends SearchResultSet {
}
}
}
-
-
-
diff --git a/includes/SearchUpdate.php b/includes/SearchUpdate.php
index 849d6dc7..087a8ba5 100644
--- a/includes/SearchUpdate.php
+++ b/includes/SearchUpdate.php
@@ -1,7 +1,7 @@
<?php
/**
* See deferred.txt
- * @addtogroup Search
+ * @ingroup Search
*/
class SearchUpdate {
@@ -95,21 +95,19 @@ class SearchUpdate {
wfProfileOut( "$fname-regexps" );
wfRunHooks( 'SearchUpdate', array( $this->mId, $this->mNamespace, $this->mTitle, &$text ) );
-
+
# Perform the actual update
$search->update($this->mId, Title::indexTitle( $this->mNamespace, $this->mTitle ),
$text);
-
+
wfProfileOut( $fname );
}
}
/**
* Placeholder class
- * @addtogroup Search
+ * @ingroup Search
*/
class SearchUpdateMyISAM extends SearchUpdate {
# Inherits everything
}
-
-
diff --git a/includes/Setup.php b/includes/Setup.php
index 53e0b949..877ea766 100644
--- a/includes/Setup.php
+++ b/includes/Setup.php
@@ -10,7 +10,7 @@
if( !defined( 'MEDIAWIKI' ) ) {
echo "This file is part of MediaWiki, it is not a valid entry point.\n";
exit( 1 );
-}
+}
# The main wiki script and things like database
# conversion and maintenance scripts all share a
@@ -58,12 +58,11 @@ if ( empty( $wgFileStore['deleted']['directory'] ) ) {
$wgFileStore['deleted']['directory'] = "{$wgUploadDirectory}/deleted";
}
-
/**
* Initialise $wgLocalFileRepo from backwards-compatible settings
*/
if ( !$wgLocalFileRepo ) {
- $wgLocalFileRepo = array(
+ $wgLocalFileRepo = array(
'class' => 'LocalRepo',
'name' => 'local',
'directory' => $wgUploadDirectory,
@@ -101,7 +100,7 @@ if ( $wgUseSharedUploads ) {
'fetchDescription' => $wgFetchCommonsDescriptions,
);
} else {
- $wgForeignFileRepos[] = array(
+ $wgForeignFileRepos[] = array(
'class' => 'FSRepo',
'name' => 'shared',
'directory' => $wgSharedUploadDirectory,
@@ -115,7 +114,16 @@ if ( $wgUseSharedUploads ) {
}
}
-require_once( "$IP/includes/AutoLoader.php" );
+/**
+ * Workaround for http://bugs.php.net/bug.php?id=45132
+ * escapeshellarg() destroys non-ASCII characters if LANG is not a UTF-8 locale
+ */
+putenv( 'LC_CTYPE=en_US.UTF-8' );
+setlocale( LC_CTYPE, 'en_US.UTF-8' );
+
+if ( !class_exists( 'AutoLoader' ) ) {
+ require_once( "$IP/includes/AutoLoader.php" );
+}
wfProfileIn( $fname.'-exception' );
require_once( "$IP/includes/Exception.php" );
@@ -159,6 +167,19 @@ if ( $wgCommandLineMode ) {
wfDebug( $_SERVER['REQUEST_METHOD'] . ' ' . $_SERVER['REQUEST_URI'] . "\n" );
}
+if( $wgRCFilterByAge ) {
+ ## Trim down $wgRCLinkDays so that it only lists links which are valid
+ ## as determined by $wgRCMaxAge.
+ ## Note that we allow 1 link higher than the max for things like 56 days but a 60 day link.
+ sort($wgRCLinkDays);
+ for( $i = 0; $i < count($wgRCLinkDays); $i++ ) {
+ if( $wgRCLinkDays[$i] >= $wgRCMaxAge / ( 3600 * 24 ) ) {
+ $wgRCLinkDays = array_slice( $wgRCLinkDays, 0, $i+1, false );
+ break;
+ }
+ }
+}
+
if ( $wgSkipSkin ) {
$wgSkipSkins[] = $wgSkipSkin;
}
@@ -181,18 +202,25 @@ $messageMemc =& wfGetMessageCacheStorage();
$parserMemc =& wfGetParserCacheStorage();
wfDebug( 'Main cache: ' . get_class( $wgMemc ) .
- "\nMessage cache: " . get_class( $messageMemc ) .
- "\nParser cache: " . get_class( $parserMemc ) . "\n" );
+ "\nMessage cache: " . get_class( $messageMemc ) .
+ "\nParser cache: " . get_class( $parserMemc ) . "\n" );
wfProfileOut( $fname.'-memcached' );
wfProfileIn( $fname.'-SetupSession' );
-if ( $wgDBprefix ) {
- $wgCookiePrefix = $wgDBname . '_' . $wgDBprefix;
-} elseif ( $wgSharedDB ) {
- $wgCookiePrefix = $wgSharedDB;
-} else {
- $wgCookiePrefix = $wgDBname;
+# Set default shared prefix
+if( $wgSharedPrefix === false ) $wgSharedPrefix = $wgDBprefix;
+
+if( !$wgCookiePrefix ) {
+ if ( in_array('user', $wgSharedTables) && $wgSharedDB && $wgSharedPrefix ) {
+ $wgCookiePrefix = $wgSharedDB . '_' . $wgSharedPrefix;
+ } elseif ( in_array('user', $wgSharedTables) && $wgSharedDB ) {
+ $wgCookiePrefix = $wgSharedDB;
+ } elseif ( $wgDBprefix ) {
+ $wgCookiePrefix = $wgDBname . '_' . $wgDBprefix;
+ } else {
+ $wgCookiePrefix = $wgDBname;
+ }
}
$wgCookiePrefix = strtr($wgCookiePrefix, "=,; +.\"'\\[", "__________");
@@ -213,20 +241,6 @@ if( !$wgCommandLineMode && ( $wgRequest->checkSessionCookie() || isset( $_COOKIE
wfProfileOut( $fname.'-SetupSession' );
wfProfileIn( $fname.'-globals' );
-if ( !$wgDBservers ) {
- $wgDBservers = array(array(
- 'host' => $wgDBserver,
- 'user' => $wgDBuser,
- 'password' => $wgDBpassword,
- 'dbname' => $wgDBname,
- 'type' => $wgDBtype,
- 'load' => 1,
- 'flags' => ($wgDebugDumpSql ? DBO_DEBUG : 0) | DBO_DEFAULT
- ));
-}
-
-$wgLoadBalancer = new StubObject( 'wgLoadBalancer', 'LoadBalancer',
- array( $wgDBservers, false, $wgMasterWaitTimeout, true ) );
$wgContLang = new StubContLang;
// Now that variant lists may be available...
@@ -237,8 +251,8 @@ $wgLang = new StubUserLang;
$wgOut = new StubObject( 'wgOut', 'OutputPage' );
$wgParser = new StubObject( 'wgParser', $wgParserConf['class'], array( $wgParserConf ) );
-$wgMessageCache = new StubObject( 'wgMessageCache', 'MessageCache',
- array( $parserMemc, $wgUseDatabaseMessages, $wgMsgCacheExpiry, wfWikiID() ) );
+$wgMessageCache = new StubObject( 'wgMessageCache', 'MessageCache',
+ array( $messageMemc, $wgUseDatabaseMessages, $wgMsgCacheExpiry, wfWikiID() ) );
wfProfileOut( $fname.'-globals' );
wfProfileIn( $fname.'-User' );
@@ -246,7 +260,7 @@ wfProfileIn( $fname.'-User' );
# Skin setup functions
# Entries can be added to this variable during the inclusion
# of the extension file. Skins can then perform any necessary initialisation.
-#
+#
foreach ( $wgSkinExtensionFunctions as $func ) {
call_user_func( $func );
}
@@ -269,8 +283,6 @@ if ( $wgAjaxUploadDestCheck ) $wgAjaxExportList[] = 'UploadForm::ajaxGetExistsWa
if( $wgAjaxLicensePreview )
$wgAjaxExportList[] = 'UploadForm::ajaxGetLicensePreview';
-wfSeedRandom();
-
# Placeholders in case of DB error
$wgTitle = null;
$wgArticle = null;
@@ -300,5 +312,3 @@ wfDebug( "Fully initialised\n" );
$wgFullyInitialised = true;
wfProfileOut( $fname.'-extensions' );
wfProfileOut( $fname );
-
-
diff --git a/includes/SiteConfiguration.php b/includes/SiteConfiguration.php
index beeeaf15..6cdd5082 100644
--- a/includes/SiteConfiguration.php
+++ b/includes/SiteConfiguration.php
@@ -54,7 +54,7 @@ class SiteConfiguration {
}
return $retval;
}
-
+
/** Type-safe string replace; won't do replacements on non-strings */
function doReplace( $from, $to, $in ) {
if( is_string( $in ) ) {
@@ -126,7 +126,11 @@ class SiteConfiguration {
$site = NULL;
$lang = NULL;
foreach ( $this->suffixes as $suffix ) {
- if ( substr( $db, -strlen( $suffix ) ) == $suffix ) {
+ if ( $suffix === '' ) {
+ $site = '';
+ $lang = $db;
+ break;
+ } elseif ( substr( $db, -strlen( $suffix ) ) == $suffix ) {
$site = $suffix == 'wiki' ? 'wikipedia' : $suffix;
$lang = substr( $db, 0, strlen( $db ) - strlen( $suffix ) );
break;
@@ -142,5 +146,3 @@ class SiteConfiguration {
}
}
}
-
-
diff --git a/includes/SiteStats.php b/includes/SiteStats.php
index d7b9161a..3b10f4a0 100644
--- a/includes/SiteStats.php
+++ b/includes/SiteStats.php
@@ -27,10 +27,10 @@ class SiteStats {
$dbr = wfGetDB( DB_SLAVE );
self::$row = $dbr->selectRow( 'site_stats', '*', false, __METHOD__ );
}
-
+
self::$loaded = true;
}
-
+
static function loadAndLazyInit() {
wfDebug( __METHOD__ . ": reading site_stats from slave\n" );
$row = self::doLoad( wfGetDB( DB_SLAVE ) );
@@ -40,24 +40,24 @@ class SiteStats {
wfDebug( __METHOD__ . ": site_stats damaged or missing on slave\n" );
$row = self::doLoad( wfGetDB( DB_MASTER ) );
}
-
+
if( !self::isSane( $row ) ) {
// Normally the site_stats table is initialized at install time.
// Some manual construction scenarios may leave the table empty or
// broken, however, for instance when importing from a dump into a
// clean schema with mwdumper.
wfDebug( __METHOD__ . ": initializing damaged or missing site_stats\n" );
-
+
global $IP;
require_once "$IP/maintenance/initStats.inc";
-
+
ob_start();
wfInitStats();
ob_end_clean();
-
+
$row = self::doLoad( wfGetDB( DB_MASTER ) );
}
-
+
if( !self::isSane( $row ) ) {
wfDebug( __METHOD__ . ": site_stats persistently nonsensical o_O\n" );
}
@@ -92,7 +92,7 @@ class SiteStats {
self::load();
return self::$row->ss_users;
}
-
+
static function images() {
self::load();
return self::$row->ss_images;
@@ -117,7 +117,7 @@ class SiteStats {
}
return self::$jobs;
}
-
+
static function pagesInNs( $ns ) {
wfProfileIn( __METHOD__ );
if( !isset( self::$pageCount[$ns] ) ) {
@@ -236,4 +236,3 @@ class SiteStatsUpdate {
*/
}
}
-
diff --git a/includes/Skin.php b/includes/Skin.php
index 30d2c2bc..a9e44ab4 100644
--- a/includes/Skin.php
+++ b/includes/Skin.php
@@ -1,22 +1,26 @@
<?php
+/**
+ * @defgroup Skins Skins
+ */
+
if ( ! defined( 'MEDIAWIKI' ) )
die( 1 );
-# See skin.txt
-
/**
* The main skin class that provide methods and properties for all other skins.
* This base class is also the "Standard" skin.
*
* See docs/skin.txt for more information.
*
- * @addtogroup Skins
+ * @ingroup Skins
*/
class Skin extends Linker {
/**#@+
* @private
*/
var $mWatchLinkNum = 0; // Appended to end of watch link id's
+ // How many search boxes have we made? Avoid duplicate id's.
+ protected $searchboxes = '';
/**#@-*/
protected $mRevisionId; // The revision ID we're looking at, null if not applicable.
protected $skinname = 'standard' ;
@@ -105,7 +109,7 @@ class Skin extends Linker {
*/
static function &newFromKey( $key ) {
global $wgStyleDirectory;
-
+
$key = Skin::normalizeKey( $key );
$skinNames = Skin::getSkinNames();
@@ -153,34 +157,30 @@ class Skin extends Linker {
}
function initPage( &$out ) {
- global $wgFavicon, $wgAppleTouchIcon, $wgScriptPath, $wgSitename, $wgContLang, $wgScriptExtension;
+ global $wgFavicon, $wgAppleTouchIcon, $wgScriptPath, $wgScriptExtension;
wfProfileIn( __METHOD__ );
if( false !== $wgFavicon ) {
$out->addLink( array( 'rel' => 'shortcut icon', 'href' => $wgFavicon ) );
}
-
+
if( false !== $wgAppleTouchIcon ) {
$out->addLink( array( 'rel' => 'apple-touch-icon', 'href' => $wgAppleTouchIcon ) );
- }
-
- $code = $wgContLang->getCode();
- $name = $wgContLang->getLanguageName( $code );
- $langName = $name ? $name : $code;
+ }
# OpenSearch description link
- $out->addLink( array(
- 'rel' => 'search',
+ $out->addLink( array(
+ 'rel' => 'search',
'type' => 'application/opensearchdescription+xml',
'href' => "$wgScriptPath/opensearch_desc{$wgScriptExtension}",
- 'title' => "$wgSitename ($langName)",
+ 'title' => wfMsgForContent( 'opensearch-desc' ),
));
$this->addMetadataLinks($out);
$this->mRevisionId = $out->mRevisionId;
-
+
$this->preloadExistence();
wfProfileOut( __METHOD__ );
@@ -207,7 +207,7 @@ class Skin extends Linker {
$lb = new LinkBatch( $titles );
$lb->execute();
}
-
+
function addMetadataLinks( &$out ) {
global $wgTitle, $wgEnableDublinCoreRdf, $wgEnableCreativeCommonsRdf;
global $wgRightsPage, $wgRightsUrl;
@@ -271,7 +271,7 @@ class Skin extends Linker {
$out->out( $this->bottomScripts() );
- $out->out( $out->reportTime() );
+ $out->out( wfReportTime() );
$out->out( "\n</body></html>" );
wfProfileOut( __METHOD__ );
@@ -300,19 +300,23 @@ class Skin extends Linker {
global $wgScript, $wgStylePath, $wgUser;
global $wgArticlePath, $wgScriptPath, $wgServer, $wgContLang, $wgLang;
global $wgTitle, $wgCanonicalNamespaceNames, $wgOut, $wgArticle;
- global $wgBreakFrames, $wgRequest;
+ global $wgBreakFrames, $wgRequest, $wgVariantArticlePath, $wgActionPaths;
global $wgUseAjax, $wgAjaxWatch;
global $wgVersion, $wgEnableAPI, $wgEnableWriteAPI;
+ global $wgRestrictionTypes, $wgLivePreview;
+ global $wgMWSuggestTemplate, $wgDBname, $wgEnableMWSuggest;
$ns = $wgTitle->getNamespace();
$nsname = isset( $wgCanonicalNamespaceNames[ $ns ] ) ? $wgCanonicalNamespaceNames[ $ns ] : $wgTitle->getNsText();
- $vars = array(
+ $vars = array(
'skin' => $data['skinname'],
'stylepath' => $wgStylePath,
'wgArticlePath' => $wgArticlePath,
'wgScriptPath' => $wgScriptPath,
'wgScript' => $wgScript,
+ 'wgVariantArticlePath' => $wgVariantArticlePath,
+ 'wgActionPaths' => $wgActionPaths,
'wgServer' => $wgServer,
'wgCanonicalNamespace' => $nsname,
'wgCanonicalSpecialPageName' => SpecialPage::resolveAlias( $wgTitle->getDBkey() ),
@@ -320,8 +324,6 @@ class Skin extends Linker {
'wgPageName' => $wgTitle->getPrefixedDBKey(),
'wgTitle' => $wgTitle->getText(),
'wgAction' => $wgRequest->getText( 'action', 'view' ),
- 'wgRestrictionEdit' => $wgTitle->getRestrictions( 'edit' ),
- 'wgRestrictionMove' => $wgTitle->getRestrictions( 'move' ),
'wgArticleId' => $wgTitle->getArticleId(),
'wgIsArticle' => $wgOut->isArticle(),
'wgUserName' => $wgUser->isAnon() ? NULL : $wgUser->getName(),
@@ -334,8 +336,17 @@ class Skin extends Linker {
'wgEnableAPI' => $wgEnableAPI,
'wgEnableWriteAPI' => $wgEnableWriteAPI,
);
+
+ if( $wgUseAjax && $wgEnableMWSuggest && !$wgUser->getOption( 'disablesuggest', false )){
+ $vars['wgMWSuggestTemplate'] = SearchEngine::getMWSuggestTemplate();
+ $vars['wgDBname'] = $wgDBname;
+ $vars['wgSearchNamespaces'] = SearchEngine::userNamespaces( $wgUser );
+ $vars['wgMWSuggestMessages'] = array( wfMsg('search-mwsuggest-enabled'), wfMsg('search-mwsuggest-disabled'));
+ }
+
+ foreach( $wgRestrictionTypes as $type )
+ $vars['wgRestriction' . ucfirst( $type )] = $wgTitle->getRestrictions( $type );
- global $wgLivePreview;
if ( $wgLivePreview && $wgUser->getOption( 'uselivepreview' ) ) {
$vars['wgLivepreviewMessageLoading'] = wfMsg( 'livepreview-loading' );
$vars['wgLivepreviewMessageReady'] = wfMsg( 'livepreview-ready' );
@@ -507,7 +518,7 @@ a.stub:after, #quickbar a.stub:after {
END;
}
if( $wgUser->getOption( 'justify' ) ) {
- $s .= "#article, #bodyContent { text-align: justify; }\n";
+ $s .= "#article, #bodyContent, #mw_content { text-align: justify; }\n";
}
if( !$wgUser->getOption( 'showtoc' ) ) {
$s .= "#toc { display: none; }\n";
@@ -535,14 +546,10 @@ END;
}
$a['onload'] = $wgOut->getOnloadHandler();
- if( $wgUser->getOption( 'editsectiononrightclick' ) ) {
- if( $a['onload'] != '' ) {
- $a['onload'] .= ';';
- }
- $a['onload'] .= 'setupRightClickEdit()';
- }
- $a['class'] = 'ns-'.$wgTitle->getNamespace().' '.($wgContLang->isRTL() ? "rtl" : "ltr").
- ' '.Sanitizer::escapeClass( 'page-'.$wgTitle->getPrefixedText() );
+ $a['class'] =
+ 'mediawiki ns-'.$wgTitle->getNamespace().
+ ' '.($wgContLang->isRTL() ? "rtl" : "ltr").
+ ' '.Sanitizer::escapeClass( 'page-'.$wgTitle->getPrefixedText() );
return $a;
}
@@ -625,9 +632,9 @@ END;
}
- function getCategoryLinks () {
+ function getCategoryLinks() {
global $wgOut, $wgTitle, $wgUseCategoryBrowser;
- global $wgContLang;
+ global $wgContLang, $wgUser;
if( count( $wgOut->mCategoryLinks ) == 0 ) return '';
@@ -639,11 +646,33 @@ END;
$dir = $wgContLang->isRTL() ? 'rtl' : 'ltr';
$embed = "<span dir='$dir'>";
$pop = '</span>';
- $t = $embed . implode ( "{$pop} {$sep} {$embed}" , $wgOut->mCategoryLinks ) . $pop;
- $msg = wfMsgExt( 'pagecategories', array( 'parsemag', 'escape' ), count( $wgOut->mCategoryLinks ) );
- $s = $this->makeLinkObj( Title::newFromText( wfMsgForContent('pagecategorieslink') ), $msg )
- . ': ' . $t;
+ $allCats = $wgOut->getCategoryLinks();
+ $s = '';
+ $colon = wfMsgExt( 'colon-separator', 'escapenoentities' );
+ if ( !empty( $allCats['normal'] ) ) {
+ $t = $embed . implode ( "{$pop} {$sep} {$embed}" , $allCats['normal'] ) . $pop;
+
+ $msg = wfMsgExt( 'pagecategories', array( 'parsemag', 'escapenoentities' ), count( $allCats['normal'] ) );
+ $s .= '<div id="mw-normal-catlinks">' .
+ $this->makeLinkObj( Title::newFromText( wfMsgForContent('pagecategorieslink') ), $msg )
+ . $colon . $t . '</div>';
+ }
+
+ # Hidden categories
+ if ( isset( $allCats['hidden'] ) ) {
+ if ( $wgUser->getBoolOption( 'showhiddencats' ) ) {
+ $class ='mw-hidden-cats-user-shown';
+ } elseif ( $wgTitle->getNamespace() == NS_CATEGORY ) {
+ $class = 'mw-hidden-cats-ns-shown';
+ } else {
+ $class = 'mw-hidden-cats-hidden';
+ }
+ $s .= "<div id=\"mw-hidden-catlinks\" class=\"$class\">" .
+ wfMsgExt( 'hidden-categories', array( 'parsemag', 'escapenoentities' ), count( $allCats['hidden'] ) ) .
+ $colon . $embed . implode( "$pop $sep $embed", $allCats['hidden'] ) . $pop .
+ "</div>";
+ }
# optional 'dmoz-like' category browser. Will be shown under the list
# of categories an article belong to
@@ -689,8 +718,16 @@ END;
function getCategories() {
$catlinks=$this->getCategoryLinks();
- if(!empty($catlinks)) {
- return "<p class='catlinks'>{$catlinks}</p>";
+
+ $classes = 'catlinks';
+
+ if( strpos( $catlinks, '<div id="mw-normal-catlinks">' ) === false &&
+ strpos( $catlinks, '<div id="mw-hidden-catlinks" class="mw-hidden-cats-hidden">' ) !== false ) {
+ $classes .= ' catlinks-allhidden';
+ }
+
+ if( !empty( $catlinks ) ){
+ return "<div id='catlinks' class='$classes'>{$catlinks}</div>";
}
}
@@ -700,7 +737,7 @@ END;
/**
* This gets called shortly before the \</body\> tag.
- * @return String HTML to be put before \</body\>
+ * @return String HTML to be put before \</body\>
*/
function afterContent() {
$printfooter = "<div class=\"printfooter\">\n" . $this->printFooter() . "</div>\n";
@@ -709,7 +746,7 @@ END;
/**
* This gets called shortly before the \</body\> tag.
- * @return String HTML-wrapped JS code to be put before \</body\>
+ * @return String HTML-wrapped JS code to be put before \</body\>
*/
function bottomScripts() {
global $wgJsMimeType;
@@ -843,29 +880,35 @@ END;
function subPageSubtitle() {
$subpages = '';
if(!wfRunHooks('SkinSubPageSubtitle', array(&$subpages)))
- return $retval;
+ return $subpages;
- global $wgOut, $wgTitle, $wgNamespacesWithSubpages;
- if($wgOut->isArticle() && !empty($wgNamespacesWithSubpages[$wgTitle->getNamespace()])) {
+ global $wgOut, $wgTitle;
+ if($wgOut->isArticle() && MWNamespace::hasSubpages( $wgTitle->getNamespace() )) {
$ptext=$wgTitle->getPrefixedText();
if(preg_match('/\//',$ptext)) {
$links = explode('/',$ptext);
+ array_pop( $links );
$c = 0;
$growinglink = '';
+ $display = '';
foreach($links as $link) {
- $c++;
- if ($c<count($links)) {
- $growinglink .= $link;
- $getlink = $this->makeLink( $growinglink, htmlspecialchars( $link ) );
- if(preg_match('/class="new"/i',$getlink)) { break; } # this is a hack, but it saves time
+ $growinglink .= $link;
+ $display .= $link;
+ $linkObj = Title::newFromText( $growinglink );
+ if( is_object( $linkObj ) && $linkObj->exists() ){
+ $getlink = $this->makeKnownLinkObj( $linkObj, htmlspecialchars( $display ) );
+ $c++;
if ($c>1) {
$subpages .= ' | ';
} else {
$subpages .= '&lt; ';
}
$subpages .= $getlink;
- $growinglink .= '/';
+ $display = '';
+ } else {
+ $display .= '/';
}
+ $growinglink .= '/';
}
}
}
@@ -903,9 +946,12 @@ END;
$q = '';
} else { $q = "returnto={$rt}"; }
+ $loginlink = $wgUser->isAllowed( 'createaccount' )
+ ? 'nav-login-createaccount'
+ : 'login';
$s .= "\n<br />" . $this->makeKnownLinkObj(
SpecialPage::getTitleFor( 'Userlogin' ),
- wfMsg( 'login' ), $q );
+ wfMsg( $loginlink ), $q );
} else {
$n = $wgUser->getName();
$rt = $wgTitle->getPrefixedURL();
@@ -939,13 +985,16 @@ END;
global $wgRequest;
$search = $wgRequest->getText( 'search' );
- $s = '<form name="search" class="inline" method="post" action="'
+ $s = '<form id="searchform'.$this->searchboxes.'" name="search" class="inline" method="post" action="'
. $this->escapeSearchLink() . "\">\n"
- . '<input type="text" name="search" size="19" value="'
+ . '<input type="text" id="searchInput'.$this->searchboxes.'" name="search" size="19" value="'
. htmlspecialchars(substr($search,0,256)) . "\" />\n"
. '<input type="submit" name="go" value="' . wfMsg ('searcharticle') . '" />&nbsp;'
. '<input type="submit" name="fulltext" value="' . wfMsg ('searchbutton') . "\" />\n</form>";
+ // Ensure unique id's for search boxes made after the first
+ $this->searchboxes = $this->searchboxes == '' ? 2 : $this->searchboxes + 1;
+
return $s;
}
@@ -962,14 +1011,14 @@ END;
}
# Many people don't like this dropdown box
#$s .= $sep . $this->specialPagesList();
-
+
$s .= $this->variantLinks();
-
+
$s .= $this->extensionTabLinks();
return $s;
}
-
+
/**
* Compatibility for extensions adding functionality through tabs.
* Eventually these old skins should be replaced with SkinTemplate-based
@@ -987,7 +1036,7 @@ END;
}
return $s;
}
-
+
/**
* Language/charset variant links for classic-style skins
* @return string
@@ -1154,9 +1203,12 @@ END;
}
function lastModified() {
- global $wgLang, $wgArticle, $wgLoadBalancer;
-
- $timestamp = $wgArticle->getTimestamp();
+ global $wgLang, $wgArticle;
+ if( $this->mRevisionId ) {
+ $timestamp = Revision::getTimestampFromId( $this->mRevisionId, $wgArticle->getId() );
+ } else {
+ $timestamp = $wgArticle->getTimestamp();
+ }
if ( $timestamp ) {
$d = $wgLang->date( $timestamp, true );
$t = $wgLang->time( $timestamp, true );
@@ -1164,7 +1216,7 @@ END;
} else {
$s = '';
}
- if ( $wgLoadBalancer->getLaggedSlaveMode() ) {
+ if ( wfGetLB()->getLaggedSlaveMode() ) {
$s .= ' <strong>' . wfMsg( 'laggedslavemode' ) . '</strong>';
}
return $s;
@@ -1253,11 +1305,13 @@ END;
function editThisPage() {
global $wgOut, $wgTitle;
- if ( ! $wgOut->isArticleRelated() ) {
+ if ( !$wgOut->isArticleRelated() ) {
$s = wfMsg( 'protectedpage' );
} else {
- if ( $wgTitle->userCan( 'edit' ) ) {
+ if( $wgTitle->userCan( 'edit' ) && $wgTitle->exists() ) {
$t = wfMsg( 'editthispage' );
+ } elseif( $wgTitle->userCan( 'create' ) && !$wgTitle->exists() ) {
+ $t = wfMsg( 'create-this-page' );
} else {
$t = wfMsg( 'viewsource' );
}
@@ -1360,15 +1414,15 @@ END;
function whatLinksHere() {
global $wgTitle;
- return $this->makeKnownLinkObj(
- SpecialPage::getTitleFor( 'Whatlinkshere', $wgTitle->getPrefixedDBkey() ),
+ return $this->makeKnownLinkObj(
+ SpecialPage::getTitleFor( 'Whatlinkshere', $wgTitle->getPrefixedDBkey() ),
wfMsg( 'whatlinkshere' ) );
}
function userContribsLink() {
global $wgTitle;
- return $this->makeKnownLinkObj(
+ return $this->makeKnownLinkObj(
SpecialPage::getTitleFor( 'Contributions', $wgTitle->getDBkey() ),
wfMsg( 'contributions' ) );
}
@@ -1387,7 +1441,7 @@ END;
function emailUserLink() {
global $wgTitle;
- return $this->makeKnownLinkObj(
+ return $this->makeKnownLinkObj(
SpecialPage::getTitleFor( 'Emailuser', $wgTitle->getDBkey() ),
wfMsg( 'emailuser' ) );
}
@@ -1398,8 +1452,8 @@ END;
if ( ! $wgOut->isArticleRelated() ) {
return '(' . wfMsg( 'notanarticle' ) . ')';
} else {
- return $this->makeKnownLinkObj(
- SpecialPage::getTitleFor( 'Recentchangeslinked', $wgTitle->getPrefixedDBkey() ),
+ return $this->makeKnownLinkObj(
+ SpecialPage::getTitleFor( 'Recentchangeslinked', $wgTitle->getPrefixedDBkey() ),
wfMsg( 'recentchangeslinked' ) );
}
}
@@ -1502,7 +1556,7 @@ END;
if ( $wgTitle->getNamespace() == NS_SPECIAL ) {
return '';
}
-
+
# __NEWSECTIONLINK___ changes behaviour here
# If it's present, the link points to this page, otherwise
# it points to the talk page
@@ -1513,7 +1567,7 @@ END;
} else {
$title = $wgTitle->getTalkPage();
}
-
+
return $this->makeKnownLinkObj( $title, wfMsg( 'postcomment' ), 'action=edit&section=new' );
}
@@ -1599,24 +1653,18 @@ END;
* Build an array that represents the sidebar(s), the navigation bar among them
*
* @return array
- * @private
*/
function buildSidebar() {
global $parserMemc, $wgEnableSidebarCache, $wgSidebarCacheExpiry;
- global $wgLang, $wgContLang;
-
- $fname = 'SkinTemplate::buildSidebar';
+ global $wgLang;
+ wfProfileIn( __METHOD__ );
- wfProfileIn( $fname );
+ $key = wfMemcKey( 'sidebar', $wgLang->getCode() );
- $key = wfMemcKey( 'sidebar' );
- $cacheSidebar = $wgEnableSidebarCache &&
- ($wgLang->getCode() == $wgContLang->getCode());
-
- if ($cacheSidebar) {
+ if ( $wgEnableSidebarCache ) {
$cachedsidebar = $parserMemc->get( $key );
- if ($cachedsidebar!="") {
- wfProfileOut($fname);
+ if ( $cachedsidebar ) {
+ wfProfileOut( __METHOD__ );
return $cachedsidebar;
}
}
@@ -1629,10 +1677,15 @@ END;
continue;
if (strpos($line, '**') !== 0) {
$line = trim($line, '* ');
- $heading = $line;
+ if ( $line == 'SEARCH' || $line == 'TOOLBOX' || $line == 'LANGUAGES' ) {
+ # Special box type
+ $bar[$line] = array();
+ } else {
+ $heading = $line;
+ }
} else {
if (strpos($line, '|') !== false) { // sanity check
- $line = explode( '|' , trim($line, '* '), 2 );
+ $line = array_map('trim', explode( '|' , trim($line, '* '), 2 ) );
$link = wfMsgForContent( $line[0] );
if ($link == '-')
continue;
@@ -1662,10 +1715,8 @@ END;
} else { continue; }
}
}
- if ($cacheSidebar)
- $parserMemc->set( $key, $bar, $wgSidebarCacheExpiry );
- wfProfileOut( $fname );
+ if ( $wgEnableSidebarCache ) $parserMemc->set( $key, $bar, $wgSidebarCacheExpiry );
+ wfProfileOut( __METHOD__ );
return $bar;
}
-
}
diff --git a/includes/SkinTemplate.php b/includes/SkinTemplate.php
index 0178b866..c60cfb4e 100644
--- a/includes/SkinTemplate.php
+++ b/includes/SkinTemplate.php
@@ -22,7 +22,7 @@ if ( ! defined( 'MEDIAWIKI' ) )
* to be passed to the template engine.
*
* @private
- * @addtogroup Skins
+ * @ingroup Skins
*/
class MediaWiki_I18N {
var $_context = array();
@@ -32,8 +32,7 @@ class MediaWiki_I18N {
}
function translate($value) {
- $fname = 'SkinTemplate-translate';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
// Hack for i18n:attributes in PHPTAL 1.0.0 dev version as of 2004-10-23
$value = preg_replace( '/^string:/', '', $value );
@@ -48,7 +47,7 @@ class MediaWiki_I18N {
wfRestoreWarnings();
$value = str_replace($src, $varValue, $value);
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $value;
}
}
@@ -63,7 +62,7 @@ class MediaWiki_I18N {
* to the computations individual esi snippets need. Most importantly no body
* parsing for most of those of course.
*
- * @addtogroup Skins
+ * @ingroup Skins
*/
class SkinTemplate extends Skin {
/**#@+
@@ -88,6 +87,12 @@ 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;
+
/**#@-*/
/**
@@ -102,6 +107,7 @@ class SkinTemplate extends Skin {
$this->skinname = 'monobook';
$this->stylename = 'monobook';
$this->template = 'QuickTemplate';
+ $this->cssfiles = array();
}
/**
@@ -136,17 +142,12 @@ class SkinTemplate extends Skin {
global $wgUseTrackbacks;
global $wgArticlePath, $wgScriptPath, $wgServer, $wgLang, $wgCanonicalNamespaceNames;
- $fname = 'SkinTemplate::outputPage';
- wfProfileIn( $fname );
-
- // Hook that allows last minute changes to the output page, e.g.
- // adding of CSS or Javascript by extensions.
- wfRunHooks( 'BeforePageDisplay', array( &$out ) );
+ wfProfileIn( __METHOD__ );
$oldid = $wgRequest->getVal( 'oldid' );
$diff = $wgRequest->getVal( 'diff' );
- wfProfileIn( "$fname-init" );
+ wfProfileIn( __METHOD__."-init" );
$this->initPage( $out );
$this->mTitle =& $wgTitle;
@@ -157,9 +158,9 @@ class SkinTemplate extends Skin {
#if ( $wgUseDatabaseMessages ) { // uncomment this to fall back to GetText
$tpl->setTranslator(new MediaWiki_I18N());
#}
- wfProfileOut( "$fname-init" );
+ wfProfileOut( __METHOD__."-init" );
- wfProfileIn( "$fname-stuff" );
+ wfProfileIn( __METHOD__."-stuff" );
$this->thispage = $this->mTitle->getPrefixedDbKey();
$this->thisurl = $this->mTitle->getPrefixedURL();
$this->loggedin = $wgUser->isLoggedIn();
@@ -181,9 +182,9 @@ class SkinTemplate extends Skin {
$this->setupUserCss();
$this->setupUserJs( $out->isUserJsAllowed() );
$this->titletxt = $this->mTitle->getPrefixedText();
- wfProfileOut( "$fname-stuff" );
+ wfProfileOut( __METHOD__."-stuff" );
- wfProfileIn( "$fname-stuff2" );
+ wfProfileIn( __METHOD__."-stuff2" );
$tpl->set( 'title', $wgOut->getPageTitle() );
$tpl->set( 'pagetitle', $wgOut->getHTMLTitle() );
$tpl->set( 'displaytitle', $wgOut->mPageLinkTitle );
@@ -273,6 +274,7 @@ 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);
@@ -320,9 +322,9 @@ class SkinTemplate extends Skin {
} else {
$ntl = '';
}
- wfProfileOut( "$fname-stuff2" );
+ wfProfileOut( __METHOD__."-stuff2" );
- wfProfileIn( "$fname-stuff3" );
+ wfProfileIn( __METHOD__."-stuff3" );
$tpl->setRef( 'newtalk', $ntl );
$tpl->setRef( 'skin', $this);
$tpl->set( 'logo', $this->logoText() );
@@ -387,9 +389,9 @@ class SkinTemplate extends Skin {
$tpl->set('credits', false);
$tpl->set('numberofwatchingusers', false);
}
- wfProfileOut( "$fname-stuff3" );
+ wfProfileOut( __METHOD__."-stuff3" );
- wfProfileIn( "$fname-stuff4" );
+ wfProfileIn( __METHOD__."-stuff4" );
$tpl->set( 'copyrightico', $this->getCopyrightIcon() );
$tpl->set( 'poweredbyico', $this->getPoweredBy() );
$tpl->set( 'disclaimer', $this->disclaimerLink() );
@@ -397,7 +399,7 @@ class SkinTemplate extends Skin {
$tpl->set( 'about', $this->aboutLink() );
$tpl->setRef( 'debug', $out->mDebugtext );
- $tpl->set( 'reporttime', $out->reportTime() );
+ $tpl->set( 'reporttime', wfReportTime() );
$tpl->set( 'sitenotice', wfGetSiteNotice() );
$tpl->set( 'bottomscripts', $this->bottomScripts() );
@@ -426,7 +428,7 @@ class SkinTemplate extends Skin {
} else {
$tpl->set('language_urls', false);
}
- wfProfileOut( "$fname-stuff4" );
+ wfProfileOut( __METHOD__."-stuff4" );
# Personal toolbar
$tpl->set('personal_urls', $this->buildPersonalUrls());
@@ -441,11 +443,7 @@ class SkinTemplate extends Skin {
} else {
$tpl->set('body_ondblclick', false);
}
- if( $this->iseditable && $wgUser->getOption( 'editsectiononrightclick' ) ) {
- $tpl->set( 'body_onload', 'setupRightClickEdit()' );
- } else {
- $tpl->set( 'body_onload', false );
- }
+ $tpl->set( 'body_onload', false );
$tpl->set( 'sidebar', $this->buildSidebar() );
$tpl->set( 'nav_urls', $this->buildNavUrls() );
@@ -455,13 +453,13 @@ class SkinTemplate extends Skin {
}
// execute template
- wfProfileIn( "$fname-execute" );
+ wfProfileIn( __METHOD__."-execute" );
$res = $tpl->execute();
- wfProfileOut( "$fname-execute" );
+ wfProfileOut( __METHOD__."-execute" );
// result may be an error
$this->printOrError( $res );
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
}
/**
@@ -484,9 +482,8 @@ class SkinTemplate extends Skin {
function buildPersonalUrls() {
global $wgTitle, $wgRequest;
- $fname = 'SkinTemplate::buildPersonalUrls';
$pageurl = $wgTitle->getLocalURL();
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
/* set up the default links for the personal toolbar */
$personal_urls = array();
@@ -516,14 +513,14 @@ class SkinTemplate extends Skin {
'href' => $href,
'active' => ( $href == $pageurl )
);
-
+
# We need to do an explicit check for Special:Contributions, as we
# have to match both the title, and the target (which could come
# from request values or be specified in "sub page" form. The plot
# thickens, because $wgTitle is altered for special pages, so doesn't
# contain the original alias-with-subpage.
$title = Title::newFromText( $wgRequest->getText( 'title' ) );
- if( $title instanceof Title && $title->getNamespace() == NS_SPECIAL ) {
+ if( $title instanceof Title && $title->getNamespace() == NS_SPECIAL ) {
list( $spName, $spPar ) =
SpecialPage::resolveAliasWithSubpage( $title->getText() );
$active = $spName == 'Contributions'
@@ -532,7 +529,7 @@ class SkinTemplate extends Skin {
} else {
$active = false;
}
-
+
$href = self::makeSpecialUrlSubpage( 'Contributions', $this->username );
$personal_urls['mycontris'] = array(
'text' => wfMsg( 'mycontris' ),
@@ -547,6 +544,10 @@ class SkinTemplate extends Skin {
'active' => false
);
} else {
+ global $wgUser;
+ $loginlink = $wgUser->isAllowed( 'createaccount' )
+ ? 'nav-login-createaccount'
+ : 'login';
if( $this->showIPinHeader() ) {
$href = &$this->userpageUrlDetails['href'];
$personal_urls['anonuserpage'] = array(
@@ -564,14 +565,14 @@ class SkinTemplate extends Skin {
'active' => ( $pageurl == $href )
);
$personal_urls['anonlogin'] = array(
- 'text' => wfMsg('userlogin'),
+ 'text' => wfMsg( $loginlink ),
'href' => self::makeSpecialUrl( 'Userlogin', 'returnto=' . $this->thisurl ),
'active' => $wgTitle->isSpecial( 'Userlogin' )
);
} else {
$personal_urls['login'] = array(
- 'text' => wfMsg('userlogin'),
+ 'text' => wfMsg( $loginlink ),
'href' => self::makeSpecialUrl( 'Userlogin', 'returnto=' . $this->thisurl ),
'active' => $wgTitle->isSpecial( 'Userlogin' )
);
@@ -579,7 +580,7 @@ class SkinTemplate extends Skin {
}
wfRunHooks( 'PersonalUrls', array( &$personal_urls, &$wgTitle ) );
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $personal_urls;
}
@@ -596,9 +597,9 @@ class SkinTemplate extends Skin {
$text = wfMsg( $message );
if ( wfEmptyMsg( $message, $text ) ) {
global $wgContLang;
- $text = $wgContLang->getFormattedNsText( Namespace::getSubject( $title->getNamespace() ) );
+ $text = $wgContLang->getFormattedNsText( MWNamespace::getSubject( $title->getNamespace() ) );
}
-
+
$result = array();
if( !wfRunHooks('SkinTemplateTabAction', array(&$this,
$title, $message, $selected, $checkEdit,
@@ -642,8 +643,7 @@ class SkinTemplate extends Skin {
*/
function buildContentActionUrls () {
global $wgContLang, $wgLang, $wgOut;
- $fname = 'SkinTemplate::buildContentActionUrls';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
global $wgUser, $wgRequest;
$action = $wgRequest->getText( 'action' );
@@ -671,13 +671,15 @@ class SkinTemplate extends Skin {
'',
true);
- wfProfileIn( "$fname-edit" );
+ wfProfileIn( __METHOD__."-edit" );
if ( $this->mTitle->quickUserCan( 'edit' ) && ( $this->mTitle->exists() || $this->mTitle->quickUserCan( 'create' ) ) ) {
$istalk = $this->mTitle->isTalkPage();
$istalkclass = $istalk?' istalk':'';
$content_actions['edit'] = array(
'class' => ((($action == 'edit' or $action == 'submit') and $section != 'new') ? 'selected' : '').$istalkclass,
- 'text' => wfMsg('edit'),
+ 'text' => $this->mTitle->exists()
+ ? wfMsg( 'edit' )
+ : wfMsg( 'create' ),
'href' => $this->mTitle->getLocalUrl( $this->editUrlOptions() )
);
@@ -695,9 +697,9 @@ class SkinTemplate extends Skin {
'href' => $this->mTitle->getLocalUrl( $this->editUrlOptions() )
);
}
- wfProfileOut( "$fname-edit" );
+ wfProfileOut( __METHOD__."-edit" );
- wfProfileIn( "$fname-live" );
+ wfProfileIn( __METHOD__."-live" );
if ( $this->mTitle->getArticleId() ) {
$content_actions['history'] = array(
@@ -770,7 +772,7 @@ class SkinTemplate extends Skin {
}
}
- wfProfileOut( "$fname-live" );
+ wfProfileOut( __METHOD__."-live" );
if( $this->loggedin ) {
if( !$this->mTitle->userIsWatching()) {
@@ -787,7 +789,7 @@ class SkinTemplate extends Skin {
);
}
}
-
+
wfRunHooks( 'SkinTemplateTabs', array( &$this , &$content_actions ) ) ;
} else {
@@ -824,7 +826,7 @@ class SkinTemplate extends Skin {
wfRunHooks( 'SkinTemplateContentActions', array( &$content_actions ) );
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $content_actions;
}
@@ -836,14 +838,11 @@ class SkinTemplate extends Skin {
* @private
*/
function buildNavUrls () {
- global $wgUseTrackbacks, $wgTitle, $wgArticle;
-
- $fname = 'SkinTemplate::buildNavUrls';
- wfProfileIn( $fname );
-
- global $wgUser, $wgRequest;
+ global $wgUseTrackbacks, $wgTitle, $wgUser, $wgRequest;
global $wgEnableUploads, $wgUploadNavigationUrl;
+ wfProfileIn( __METHOD__ );
+
$action = $wgRequest->getText( 'action' );
$nav_urls = array();
@@ -880,7 +879,7 @@ class SkinTemplate extends Skin {
'href' => $wgTitle->getLocalURL( "oldid=$this->mRevisionId" )
);
}
-
+
// Copy in case this undocumented, shady hook tries to mess with internals
$revid = $this->mRevisionId;
wfRunHooks( 'SkinTemplateBuildNavUrlsNav_urlsAfterPermalink', array( &$this, &$nav_urls, &$revid, &$revid ) );
@@ -917,7 +916,7 @@ class SkinTemplate extends Skin {
$nav_urls['contributions'] = array(
'href' => self::makeSpecialUrlSubpage( 'Contributions', $this->mTitle->getText() )
);
-
+
if( $id ) {
$logPage = SpecialPage::getTitleFor( 'Log' );
$nav_urls['log'] = array( 'href' => $logPage->getLocalUrl( 'user='
@@ -944,7 +943,7 @@ class SkinTemplate extends Skin {
'href' => self::makeSpecialUrlSubpage( 'Emailuser', $this->mTitle->getText() )
);
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $nav_urls;
}
@@ -961,8 +960,7 @@ class SkinTemplate extends Skin {
* @private
*/
function setupUserCss() {
- $fname = 'SkinTemplate::setupUserCss';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
global $wgRequest, $wgAllowUserCss, $wgUseSiteCss, $wgContLang, $wgSquidMaxage, $wgStylePath, $wgUser;
@@ -993,7 +991,7 @@ class SkinTemplate extends Skin {
$siteargs .= '&ts=' . $wgUser->mTouched;
}
- if( $wgContLang->isRTL() ) {
+ if( $wgContLang->isRTL() && in_array( 'rtl', $this->cssfiles ) ) {
global $wgStyleVersion;
$sitecss .= "@import \"$wgStylePath/$this->stylename/rtl.css?$wgStyleVersion\";\n";
}
@@ -1014,15 +1012,14 @@ class SkinTemplate extends Skin {
if ( !empty($sitecss) || !empty($usercss) ) {
$this->usercss = "/*<![CDATA[*/\n" . $sitecss . $usercss . '/*]]>*/';
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
}
/**
* @private
*/
function setupUserJs( $allowUserJs ) {
- $fname = 'SkinTemplate::setupUserJs';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
global $wgRequest, $wgJsMimeType;
$action = $wgRequest->getText('action');
@@ -1032,10 +1029,10 @@ class SkinTemplate extends Skin {
# XXX: additional security check/prompt?
$this->userjsprev = '/*<![CDATA[*/ ' . $wgRequest->getText('wpTextbox1') . ' /*]]>*/';
} else {
- $this->userjs = self::makeUrl($this->userpage.'/'.$this->skinname.'.js', 'action=raw&ctype='.$wgJsMimeType.'&dontcountme=s');
+ $this->userjs = self::makeUrl($this->userpage.'/'.$this->skinname.'.js', 'action=raw&ctype='.$wgJsMimeType);
}
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
}
/**
@@ -1045,31 +1042,38 @@ class SkinTemplate extends Skin {
* @private
*/
function setupPageCss() {
- $fname = 'SkinTemplate::setupPageCss';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
$out = false;
wfRunHooks( 'SkinTemplateSetupPageCss', array( &$out ) );
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $out;
}
/**
* returns css with user-specific options
- * @public
*/
-
- function getUserStylesheet() {
- $fname = 'SkinTemplate::getUserStylesheet';
- wfProfileIn( $fname );
+ public function getUserStylesheet() {
+ wfProfileIn( __METHOD__ );
$s = "/* generated user stylesheet */\n";
$s .= $this->reallyDoGetUserStyles();
- wfProfileOut( $fname );
+ 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?
@@ -1082,11 +1086,10 @@ class SkinTemplate extends Skin {
* @return string
*/
public function getUserJs() {
- $fname = 'SkinTemplate::getUserJs';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
$s = parent::getUserJs();
- $s .= "\n\n/* MediaWiki:".ucfirst($this->skinname).".js (deprecated; migrate to Common.js!) */\n";
+ $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
@@ -1096,7 +1099,7 @@ class SkinTemplate extends Skin {
$s .= $userJS;
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $s;
}
}
@@ -1104,7 +1107,7 @@ class SkinTemplate extends Skin {
/**
* Generic wrapper for template functions, with interface
* compatible with what we use of PHPTAL 0.7.
- * @addtogroup Skins
+ * @ingroup Skins
*/
class QuickTemplate {
/**
@@ -1207,7 +1210,3 @@ class QuickTemplate {
return ($msg != '-') && ($msg != ''); # ????
}
}
-
-
-
-
diff --git a/includes/SpecialPage.php b/includes/SpecialPage.php
index c9037ea7..d6ad6e6e 100644
--- a/includes/SpecialPage.php
+++ b/includes/SpecialPage.php
@@ -2,32 +2,30 @@
/**
* SpecialPage: handling special pages and lists thereof.
*
- * To add a special page in an extension, add to $wgSpecialPages either
- * an object instance or an array containing the name and constructor
- * parameters. The latter is preferred for performance reasons.
+ * To add a special page in an extension, add to $wgSpecialPages either
+ * an object instance or an array containing the name and constructor
+ * parameters. The latter is preferred for performance reasons.
*
- * The object instantiated must be either an instance of SpecialPage or a
- * sub-class thereof. It must have an execute() method, which sends the HTML
- * for the special page to $wgOut. The parent class has an execute() method
- * which distributes the call to the historical global functions. Additionally,
- * execute() also checks if the user has the necessary access privileges
+ * The object instantiated must be either an instance of SpecialPage or a
+ * sub-class thereof. It must have an execute() method, which sends the HTML
+ * for the special page to $wgOut. The parent class has an execute() method
+ * which distributes the call to the historical global functions. Additionally,
+ * execute() also checks if the user has the necessary access privileges
* and bails out if not.
*
- * To add a core special page, use the similar static list in
+ * To add a core special page, use the similar static list in
* SpecialPage::$mList. To remove a core static special page at runtime, use
* a SpecialPage_initList hook.
*
- * @addtogroup SpecialPage
- */
-
-/**
- * @access private
+ * @file
+ * @ingroup SpecialPage
+ * @defgroup SpecialPage SpecialPage
*/
/**
* Parent special page class, also static functions for handling the special
* page list.
- * @addtogroup SpecialPage
+ * @ingroup SpecialPage
*/
class SpecialPage
{
@@ -72,7 +70,15 @@ class SpecialPage
* Query parameters that can be passed through redirects
*/
var $mAllowedRedirectParams = array();
-
+ /**
+ * List of special pages, followed by parameters.
+ * If the only parameter is a string, that is the page name.
+ * Otherwise, it is an array. The format is one of:
+ ** array( 'SpecialPage', name, right )
+ ** array( 'IncludableSpecialPage', name, right, listed? )
+ ** array( 'UnlistedSpecialPage', name, right )
+ ** array( 'SpecialRedirectToSpecial', name, page to redirect to, special page param, ... )
+ */
static public $mList = array(
'DoubleRedirects' => array( 'SpecialPage', 'DoubleRedirects' ),
'BrokenRedirects' => array( 'SpecialPage', 'BrokenRedirects' ),
@@ -84,11 +90,12 @@ class SpecialPage
'Preferences' => array( 'SpecialPage', 'Preferences' ),
'Watchlist' => array( 'SpecialPage', 'Watchlist' ),
- 'Recentchanges' => array( 'IncludableSpecialPage', 'Recentchanges' ),
+ 'Recentchanges' => 'SpecialRecentchanges',
'Upload' => array( 'SpecialPage', 'Upload' ),
'Imagelist' => array( 'SpecialPage', 'Imagelist' ),
'Newimages' => array( 'IncludableSpecialPage', 'Newimages' ),
'Listusers' => array( 'SpecialPage', 'Listusers' ),
+ 'Listgrouprights' => 'SpecialListGroupRights',
'Statistics' => array( 'SpecialPage', 'Statistics' ),
'Randompage' => 'Randompage',
'Lonelypages' => array( 'SpecialPage', 'Lonelypages' ),
@@ -109,7 +116,7 @@ class SpecialPage
'Fewestrevisions' => array( 'SpecialPage', 'Fewestrevisions' ),
'Shortpages' => array( 'SpecialPage', 'Shortpages' ),
'Longpages' => array( 'SpecialPage', 'Longpages' ),
- 'Newpages' => array( 'IncludableSpecialPage', 'Newpages' ),
+ 'Newpages' => 'SpecialNewpages',
'Ancientpages' => array( 'SpecialPage', 'Ancientpages' ),
'Deadendpages' => array( 'SpecialPage', 'Deadendpages' ),
'Protectedpages' => array( 'SpecialPage', 'Protectedpages' ),
@@ -121,7 +128,7 @@ class SpecialPage
'Contributions' => array( 'SpecialPage', 'Contributions' ),
'Emailuser' => array( 'UnlistedSpecialPage', 'Emailuser' ),
'Whatlinkshere' => array( 'SpecialPage', 'Whatlinkshere' ),
- 'Recentchangeslinked' => array( 'UnlistedSpecialPage', 'Recentchangeslinked' ),
+ 'Recentchangeslinked' => 'SpecialRecentchangeslinked',
'Movepage' => array( 'UnlistedSpecialPage', 'Movepage' ),
'Blockme' => array( 'UnlistedSpecialPage', 'Blockme' ),
'Resetpass' => array( 'UnlistedSpecialPage', 'Resetpass' ),
@@ -129,6 +136,7 @@ class SpecialPage
'Categories' => array( 'SpecialPage', 'Categories' ),
'Export' => array( 'SpecialPage', 'Export' ),
'Version' => array( 'SpecialPage', 'Version' ),
+ 'Blankpage' => array( 'UnlistedSpecialPage', 'Blankpage' ),
'Allmessages' => array( 'SpecialPage', 'Allmessages' ),
'Log' => array( 'SpecialPage', 'Log' ),
'Blockip' => array( 'SpecialPage', 'Blockip', 'block' ),
@@ -138,6 +146,7 @@ class SpecialPage
'Unlockdb' => array( 'SpecialPage', 'Unlockdb', 'siteadmin' ),
'Userrights' => 'UserrightsPage',
'MIMEsearch' => array( 'SpecialPage', 'MIMEsearch' ),
+ 'FileDuplicateSearch' => array( 'SpecialPage', 'FileDuplicateSearch' ),
'Unwatchedpages' => array( 'SpecialPage', 'Unwatchedpages', 'unwatchedpages' ),
'Listredirects' => array( 'SpecialPage', 'Listredirects' ),
'Revisiondelete' => array( 'UnlistedSpecialPage', 'Revisiondelete', 'deleterevision' ),
@@ -152,7 +161,7 @@ class SpecialPage
'Listadmins' => array( 'SpecialRedirectToSpecial', 'Listadmins', 'Listusers', 'sysop' ),
'MergeHistory' => array( 'SpecialPage', 'MergeHistory', 'mergehistory' ),
'Listbots' => array( 'SpecialRedirectToSpecial', 'Listbots', 'Listusers', 'bot' ),
- );
+ );
static public $mAliases;
static public $mListInitialised = false;
@@ -171,7 +180,7 @@ class SpecialPage
return;
}
wfProfileIn( __METHOD__ );
-
+
# Better to set this now, to avoid infinite recursion in carelessly written hooks
self::$mListInitialised = true;
@@ -185,6 +194,7 @@ class SpecialPage
if( $wgEmailAuthentication ) {
self::$mList['Confirmemail'] = 'EmailConfirmation';
+ self::$mList['Invalidateemail'] = 'EmailInvalidation';
}
# Add extension special pages
@@ -234,7 +244,7 @@ class SpecialPage
}
/**
- * Given a special page name with a possible subpage, return an array
+ * Given a special page name with a possible subpage, return an array
* where the first element is the special page name and the second is the
* subpage.
*/
@@ -250,14 +260,11 @@ class SpecialPage
}
/**
- * Add a page to the list of valid special pages. This used to be the preferred
- * method for adding special pages in extensions. It's now suggested that you add
+ * Add a page to the list of valid special pages. This used to be the preferred
+ * method for adding special pages in extensions. It's now suggested that you add
* an associative record to $wgSpecialPages. This avoids autoloading SpecialPage.
*
- * @param mixed $page Must either be an array specifying a class name and
- * constructor parameters, or an object. The object,
- * when constructed, must have an execute() method which
- * sends HTML to $wgOut.
+ * @param SpecialPage $page
* @static
*/
static function addPage( &$page ) {
@@ -268,10 +275,47 @@ class SpecialPage
}
/**
+ * Add a page to a certain display group for Special:SpecialPages
+ *
+ * @param mixed $page (SpecialPage or string)
+ * @param string $group
+ * @static
+ */
+ static function setGroup( $page, $group ) {
+ global $wgSpecialPageGroups;
+ $name = is_object($page) ? $page->mName : $page;
+ $wgSpecialPageGroups[$name] = $group;
+ }
+
+ /**
+ * Add a page to a certain display group for Special:SpecialPages
+ *
+ * @param SpecialPage $page
+ * @static
+ */
+ static function getGroup( &$page ) {
+ global $wgSpecialPageGroups;
+ static $specialPageGroupsCache = array();
+ if( isset($specialPageGroupsCache[$page->mName]) ) {
+ return $specialPageGroupsCache[$page->mName];
+ }
+ $group = wfMsg('specialpages-specialpagegroup-'.strtolower($page->mName));
+ if( $group == ''
+ || wfEmptyMsg('specialpages-specialpagegroup-'.strtolower($page->mName), $group ) ) {
+ $group = isset($wgSpecialPageGroups[$page->mName])
+ ? $wgSpecialPageGroups[$page->mName]
+ : '-';
+ }
+ if( $group == '-' ) $group = 'other';
+ $specialPageGroupsCache[$page->mName] = $group;
+ return $group;
+ }
+
+ /**
* Remove a special page from the list
- * Formerly used to disable expensive or dangerous special pages. The
+ * Formerly used to disable expensive or dangerous special pages. The
* preferred method is now to add a SpecialPage_initList hook.
- *
+ *
* @static
*/
static function removePage( $name ) {
@@ -343,6 +387,32 @@ class SpecialPage
}
/**
+ * Return categorised listable special pages which are available
+ * for the current user, and everyone.
+ * @static
+ */
+ static function getUsablePages() {
+ global $wgUser;
+ if ( !self::$mListInitialised ) {
+ self::initList();
+ }
+ $pages = array();
+
+ foreach ( self::$mList as $name => $rec ) {
+ $page = self::getPage( $name );
+ if ( $page->isListed()
+ && (
+ !$page->isRestricted()
+ || $page->userCanExecute( $wgUser )
+ )
+ ) {
+ $pages[$name] = $page;
+ }
+ }
+ return $pages;
+ }
+
+ /**
* Return categorised listable special pages for all users
* @static
*/
@@ -368,17 +438,17 @@ class SpecialPage
*/
static function getRestrictedPages() {
global $wgUser;
- if ( !self::$mListInitialised ) {
+ if( !self::$mListInitialised ) {
self::initList();
}
$pages = array();
- foreach ( self::$mList as $name => $rec ) {
+ foreach( self::$mList as $name => $rec ) {
$page = self::getPage( $name );
- if (
+ if(
$page->isListed()
- and $page->isRestricted()
- and $page->userCanExecute( $wgUser )
+ && $page->isRestricted()
+ && $page->userCanExecute( $wgUser )
) {
$pages[$name] = $page;
}
@@ -435,9 +505,9 @@ class SpecialPage
}
# Redirect to canonical alias for GET commands
- # Not for POST, we'd lose the post data, so it's best to just distribute
- # the request. Such POST requests are possible for old extensions that
- # generate self-links without being aware that their default name has
+ # Not for POST, we'd lose the post data, so it's best to just distribute
+ # the request. Such POST requests are possible for old extensions that
+ # generate self-links without being aware that their default name has
# changed.
if ( !$including && $name != $page->getLocalName() && !$wgRequest->wasPosted() ) {
$query = $_GET;
@@ -573,7 +643,7 @@ class SpecialPage
$this->mFunction = $function;
}
if ( $file === 'default' ) {
- $this->mFile = dirname(__FILE__) . "/Special{$name}.php";
+ $this->mFile = dirname(__FILE__) . "/specials/Special$name.php";
} else {
$this->mFile = $file;
}
@@ -657,7 +727,7 @@ class SpecialPage
* Default execute method
* Checks user permissions, calls the function given in mFunction
*
- * This may be overridden by subclasses.
+ * This may be overridden by subclasses.
*/
function execute( $par ) {
global $wgUser;
@@ -670,7 +740,7 @@ 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.
+ # 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 ) ) )
@@ -689,7 +759,7 @@ class SpecialPage
$msg = $wgContLang->lc( $this->name() ) . '-summary';
$out = wfMsgNoTrans( $msg );
if ( ! wfEmptyMsg( $msg, $out ) and $out !== '' and ! $this->including() ) {
- $wgOut->addWikiText( $out );
+ $wgOut->addWikiMsg( $msg );
}
}
@@ -718,7 +788,7 @@ class SpecialPage
}
/**
- * If the special page is a redirect, then get the Title object it redirects to.
+ * If the special page is a redirect, then get the Title object it redirects to.
* False otherwise.
*/
function getRedirect( $subpage ) {
@@ -738,14 +808,14 @@ class SpecialPage
if( $val = $wgRequest->getVal( $arg, false ) )
$params[] = $arg . '=' . $val;
}
-
+
return count( $params ) ? implode( '&', $params ) : false;
}
}
/**
* Shortcut to construct a special page which is unlisted by default
- * @addtogroup SpecialPage
+ * @ingroup SpecialPage
*/
class UnlistedSpecialPage extends SpecialPage
{
@@ -756,7 +826,7 @@ class UnlistedSpecialPage extends SpecialPage
/**
* Shortcut to construct an includable special page
- * @addtogroup SpecialPage
+ * @ingroup SpecialPage
*/
class IncludableSpecialPage extends SpecialPage
{
@@ -767,7 +837,7 @@ class IncludableSpecialPage extends SpecialPage
/**
* Shortcut to construct a special page alias.
- * @addtogroup SpecialPage
+ * @ingroup SpecialPage
*/
class SpecialRedirectToSpecial extends UnlistedSpecialPage {
var $redirName, $redirSubpage;
@@ -797,7 +867,7 @@ class SpecialRedirectToSpecial extends UnlistedSpecialPage {
/**
* Shortcut to construct a special page pointing to current user user's page.
- * @addtogroup SpecialPage
+ * @ingroup SpecialPage
*/
class SpecialMypage extends UnlistedSpecialPage {
function __construct() {
@@ -817,7 +887,7 @@ class SpecialMypage extends UnlistedSpecialPage {
/**
* Shortcut to construct a special page pointing to current user talk page.
- * @addtogroup SpecialPage
+ * @ingroup SpecialPage
*/
class SpecialMytalk extends UnlistedSpecialPage {
function __construct() {
@@ -837,7 +907,7 @@ class SpecialMytalk extends UnlistedSpecialPage {
/**
* Shortcut to construct a special page pointing to current user contributions.
- * @addtogroup SpecialPage
+ * @ingroup SpecialPage
*/
class SpecialMycontributions extends UnlistedSpecialPage {
function __construct() {
diff --git a/includes/SquidUpdate.php b/includes/SquidUpdate.php
index db2750cd..f69d1f0b 100644
--- a/includes/SquidUpdate.php
+++ b/includes/SquidUpdate.php
@@ -1,10 +1,13 @@
<?php
/**
* See deferred.txt
+ * @file
+ * @ingroup Cache
*/
/**
- *
+ * @todo document
+ * @ingroup Cache
*/
class SquidUpdate {
var $urlArr, $mMaxTitles;
@@ -81,7 +84,7 @@ class SquidUpdate {
echo implode("<br />\n", $urlArr) . "<br />\n";
return;
}*/
-
+
if( empty( $urlArr ) ) {
return;
}
@@ -148,8 +151,9 @@ class SquidUpdate {
/* open the remaining sockets for this server */
list($server, $port) = explode(':', $wgSquidServers[$ss]);
if(!isset($port)) $port = 80;
- $sockets[$so+1] = @fsockopen($server, $port, $error, $errstr, 2);
- @stream_set_blocking($sockets[$so+1],false);
+ $socket = @fsockopen($server, $port, $error, $errstr, 2);
+ @stream_set_blocking($socket,false);
+ $sockets[] = $socket;
}
$so++;
}
@@ -219,10 +223,10 @@ class SquidUpdate {
foreach ( $urlArr as $url ) {
if( !is_string( $url ) ) {
- wfDebugDieBacktrace( 'Bad purge URL' );
+ throw new MWException( 'Bad purge URL' );
}
$url = SquidUpdate::expand( $url );
-
+
// Construct a minimal HTCP request diagram
// as per RFC 2756
// Opcode 'CLR', no response desired, no auth
@@ -260,7 +264,7 @@ class SquidUpdate {
wfDebug( $text );
}
}
-
+
/**
* Expand local URLs to fully-qualified URLs using the internal protocol
* and host defined in $wgInternalServer. Input that's already fully-
@@ -282,4 +286,3 @@ class SquidUpdate {
return $url;
}
}
-
diff --git a/includes/Status.php b/includes/Status.php
new file mode 100644
index 00000000..185ea6e5
--- /dev/null
+++ b/includes/Status.php
@@ -0,0 +1,194 @@
+<?php
+
+/**
+ * Generic operation result class
+ * Has warning/error list, boolean status and arbitrary value
+ *
+ * "Good" means the operation was completed with no warnings or errors.
+ *
+ * "OK" means the operation was partially or wholly completed.
+ *
+ * An operation which is not OK should have errors so that the user can be
+ * informed as to what went wrong. Calling the fatal() function sets an error
+ * message and simultaneously switches off the OK flag.
+ */
+class Status {
+ var $ok = true;
+ var $value;
+
+ /** Counters for batch operations */
+ var $successCount = 0, $failCount = 0;
+
+ /*semi-private*/ var $errors = array();
+ /*semi-private*/ var $cleanCallback = false;
+
+ /**
+ * Factory function for fatal errors
+ */
+ static function newFatal( $message /*, parameters...*/ ) {
+ $params = func_get_args();
+ $result = new self;
+ call_user_func_array( array( &$result, 'error' ), $params );
+ $result->ok = false;
+ return $result;
+ }
+
+ static function newGood( $value = null ) {
+ $result = new self;
+ $result->value = $value;
+ return $result;
+ }
+
+ function setResult( $ok, $value = null ) {
+ $this->ok = $ok;
+ $this->value = $value;
+ }
+
+ function isGood() {
+ return $this->ok && !$this->errors;
+ }
+
+ function isOK() {
+ return $this->ok;
+ }
+
+ function warning( $message /*, parameters... */ ) {
+ $params = array_slice( func_get_args(), 1 );
+ $this->errors[] = array(
+ 'type' => 'warning',
+ 'message' => $message,
+ 'params' => $params );
+ }
+
+ /**
+ * Add an error, do not set fatal flag
+ * This can be used for non-fatal errors
+ */
+ function error( $message /*, parameters... */ ) {
+ $params = array_slice( func_get_args(), 1 );
+ $this->errors[] = array(
+ 'type' => 'error',
+ 'message' => $message,
+ 'params' => $params );
+ }
+
+ /**
+ * Add an error and set OK to false, indicating that the operation as a whole was fatal
+ */
+ function fatal( $message /*, parameters... */ ) {
+ $params = array_slice( func_get_args(), 1 );
+ $this->errors[] = array(
+ 'type' => 'error',
+ 'message' => $message,
+ 'params' => $params );
+ $this->ok = false;
+ }
+
+ protected function cleanParams( $params ) {
+ if ( !$this->cleanCallback ) {
+ return $params;
+ }
+ $cleanParams = array();
+ foreach ( $params as $i => $param ) {
+ $cleanParams[$i] = call_user_func( $this->cleanCallback, $param );
+ }
+ return $cleanParams;
+ }
+
+ protected function getItemXML( $item ) {
+ $params = $this->cleanParams( $item['params'] );
+ $xml = "<{$item['type']}>\n" .
+ Xml::element( 'message', null, $item['message'] ) . "\n" .
+ Xml::element( 'text', null, wfMsgReal( $item['message'], $params ) ) ."\n";
+ foreach ( $params as $param ) {
+ $xml .= Xml::element( 'param', null, $param );
+ }
+ $xml .= "</{$this->type}>\n";
+ return $xml;
+ }
+
+ /**
+ * Get the error list as XML
+ */
+ function getXML() {
+ $xml = "<errors>\n";
+ foreach ( $this->errors as $error ) {
+ $xml .= $this->getItemXML( $error );
+ }
+ $xml .= "</errors>\n";
+ return $xml;
+ }
+
+ /**
+ * Get the error list as a wikitext formatted list
+ * @param string $shortContext A short enclosing context message name, to be used
+ * when there is a single error
+ * @param string $longContext A long enclosing context message name, for a list
+ */
+ function getWikiText( $shortContext = false, $longContext = false ) {
+ if ( count( $this->errors ) == 0 ) {
+ if ( $this->ok ) {
+ $this->fatal( 'internalerror_info',
+ __METHOD__." called for a good result, this is incorrect\n" );
+ } else {
+ $this->fatal( 'internalerror_info',
+ __METHOD__.": Invalid result object: no error text but not OK\n" );
+ }
+ }
+ if ( count( $this->errors ) == 1 ) {
+ $params = array_map( 'wfEscapeWikiText', $this->cleanParams( $this->errors[0]['params'] ) );
+ $s = wfMsgReal( $this->errors[0]['message'], $params, true, false, false );
+ if ( $shortContext ) {
+ $s = wfMsgNoTrans( $shortContext, $s );
+ } elseif ( $longContext ) {
+ $s = wfMsgNoTrans( $longContext, "* $s\n" );
+ }
+ } else {
+ $s = '';
+ foreach ( $this->errors as $error ) {
+ $params = array_map( 'wfEscapeWikiText', $this->cleanParams( $error['params'] ) );
+ $s .= '* ' . wfMsgReal( $error['message'], $params, true, false, false ) . "\n";
+ }
+ if ( $longContext ) {
+ $s = wfMsgNoTrans( $longContext, $s );
+ } elseif ( $shortContext ) {
+ $s = wfMsgNoTrans( $shortContext, "\n* $s\n" );
+ }
+ }
+ return $s;
+ }
+
+ /**
+ * Merge another status object into this one
+ */
+ function merge( $other, $overwriteValue = false ) {
+ $this->errors = array_merge( $this->errors, $other->errors );
+ $this->ok = $this->ok && $other->ok;
+ if ( $overwriteValue ) {
+ $this->value = $other->value;
+ }
+ $this->successCount += $other->successCount;
+ $this->failCount += $other->failCount;
+ }
+
+ function getErrorsArray() {
+ $result = array();
+ foreach ( $this->errors as $error ) {
+ if ( $error['type'] == 'error' )
+ $result[] = $error['message'];
+ }
+ return $result;
+ }
+
+ /**
+ * Returns true if the specified message is present as a warning or error
+ */
+ function hasMessage( $msg ) {
+ foreach ( $this->errors as $error ) {
+ if ( $error['message'] === $msg ) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/includes/StreamFile.php b/includes/StreamFile.php
index 2dbbe6de..b4bf531c 100644
--- a/includes/StreamFile.php
+++ b/includes/StreamFile.php
@@ -12,7 +12,7 @@ function wfStreamFile( $fname, $headers = array() ) {
$encScript = htmlspecialchars( $_SERVER['SCRIPT_NAME'] );
echo "<html><body>
<h1>File not found</h1>
-<p>Although this PHP script ($encScript) exists, the file requested for output
+<p>Although this PHP script ($encScript) exists, the file requested for output
($encFile) does not.</p>
</body></html>
";
@@ -23,7 +23,7 @@ function wfStreamFile( $fname, $headers = array() ) {
// Cancel output buffering and gzipping if set
wfResetOutputBuffers();
-
+
$type = wfGetType( $fname );
if ( $type and $type!="unknown/unknown") {
header("Content-type: $type");
@@ -71,9 +71,7 @@ function wfGetType( $filename ) {
return 'unknown/unknown';
}
else {
- $magic=& MimeMagic::singleton();
+ $magic = MimeMagic::singleton();
return $magic->guessMimeType($filename); //full fancy mime detection
}
}
-
-
diff --git a/includes/StringUtils.php b/includes/StringUtils.php
index 374fb002..70d0bff1 100644
--- a/includes/StringUtils.php
+++ b/includes/StringUtils.php
@@ -4,14 +4,14 @@
*/
class StringUtils {
/**
- * Perform an operation equivalent to
+ * Perform an operation equivalent to
*
* preg_replace( "!$startDelim(.*?)$endDelim!", $replace, $subject );
*
* except that it's worst-case O(N) instead of O(N^2)
*
* Compared to delimiterReplace(), this implementation is fast but memory-
- * hungry and inflexible. The memory requirements are such that I don't
+ * hungry and inflexible. The memory requirements are such that I don't
* recommend using it on anything but guaranteed small chunks of text.
*/
static function hungryDelimiterReplace( $startDelim, $endDelim, $replace, $subject ) {
@@ -27,9 +27,9 @@ class StringUtils {
}
return $output;
}
-
+
/**
- * Perform an operation equivalent to
+ * Perform an operation equivalent to
*
* preg_replace_callback( "!$startDelim(.*)$endDelim!s$flags", $callback, $subject )
*
@@ -40,9 +40,9 @@ class StringUtils {
*/
# If the start delimiter ends with an initial substring of the end delimiter,
# e.g. in the case of C-style comments, the behaviour differs from the model
- # regex. In this implementation, the end must share no characters with the
- # start, so e.g. /*/ is not considered to be both the start and end of a
- # comment. /*/xy/*/ is considered to be a single comment with contents /xy/.
+ # regex. In this implementation, the end must share no characters with the
+ # start, so e.g. /*/ is not considered to be both the start and end of a
+ # comment. /*/xy/*/ is considered to be a single comment with contents /xy/.
static function delimiterReplaceCallback( $startDelim, $endDelim, $callback, $subject, $flags = '' ) {
$inputPos = 0;
$outputPos = 0;
@@ -53,13 +53,13 @@ class StringUtils {
$strcmp = strpos( $flags, 'i' ) === false ? 'strcmp' : 'strcasecmp';
$endLength = strlen( $endDelim );
$m = array();
-
- while ( $inputPos < strlen( $subject ) &&
- preg_match( "!($encStart)|($encEnd)!S$flags", $subject, $m, PREG_OFFSET_CAPTURE, $inputPos ) )
+
+ while ( $inputPos < strlen( $subject ) &&
+ preg_match( "!($encStart)|($encEnd)!S$flags", $subject, $m, PREG_OFFSET_CAPTURE, $inputPos ) )
{
$tokenOffset = $m[0][1];
if ( $m[1][0] != '' ) {
- if ( $foundStart &&
+ if ( $foundStart &&
$strcmp( $endDelim, substr( $subject, $tokenOffset, $endLength ) ) == 0 )
{
# An end match is present at the same location
@@ -112,13 +112,13 @@ class StringUtils {
}
/*
- * Perform an operation equivalent to
+ * Perform an operation equivalent to
*
* preg_replace( "!$startDelim(.*)$endDelim!$flags", $replace, $subject )
*
* @param string $startDelim Start delimiter regular expression
* @param string $endDelim End delimiter regular expression
- * @param string $replace Replacement string. May contain $1, which will be
+ * @param string $replace Replacement string. May contain $1, which will be
* replaced by the text between the delimiters
* @param string $subject String to search
* @return string The string with the matches replaced
@@ -138,10 +138,10 @@ class StringUtils {
*/
static function explodeMarkup( $separator, $text ) {
$placeholder = "\x00";
-
+
// Remove placeholder instances
$text = str_replace( $placeholder, '', $text );
-
+
// Replace instances of the separator inside HTML-like tags with the placeholder
$replacer = new DoubleReplacer( $separator, $placeholder );
$cleaned = StringUtils::delimiterReplaceCallback( '<', '>', $replacer->cb(), $text );
@@ -151,7 +151,7 @@ class StringUtils {
foreach( $items as $i => $str ) {
$items[$i] = str_replace( $placeholder, $separator, $str );
}
-
+
return $items;
}
@@ -170,7 +170,7 @@ class StringUtils {
}
/**
- * Base class for "replacers", objects used in preg_replace_callback() and
+ * Base class for "replacers", objects used in preg_replace_callback() and
* StringUtils::delimiterReplaceCallback()
*/
class Replacer {
@@ -207,7 +207,7 @@ class DoubleReplacer extends Replacer {
$this->to = $to;
$this->index = $index;
}
-
+
function replace( $matches ) {
return str_replace( $this->from, $this->to, $matches[$this->index] );
}
@@ -283,6 +283,17 @@ class ReplacementArray {
$this->fss = false;
}
+ function removePair( $from ) {
+ unset($this->data[$from]);
+ $this->fss = false;
+ }
+
+ function removeArray( $data ) {
+ foreach( $data as $from => $to )
+ $this->removePair( $from );
+ $this->fss = false;
+ }
+
function replace( $subject ) {
if ( function_exists( 'fss_prep_replace' ) ) {
wfProfileIn( __METHOD__.'-fss' );
@@ -299,5 +310,3 @@ class ReplacementArray {
return $result;
}
}
-
-
diff --git a/includes/StubObject.php b/includes/StubObject.php
index aa72c360..ec52e7f4 100644
--- a/includes/StubObject.php
+++ b/includes/StubObject.php
@@ -2,47 +2,89 @@
/**
* Class to implement stub globals, which are globals that delay loading the
- * their associated module code by deferring initialisation until the first
- * method call.
+ * their associated module code by deferring initialisation until the first
+ * method call.
*
- * Note on unstub loops:
+ * Note on unstub loops:
*
- * Unstub loops (infinite recursion) sometimes occur when a constructor calls
- * another function, and the other function calls some method of the stub. The
+ * Unstub loops (infinite recursion) sometimes occur when a constructor calls
+ * another function, and the other function calls some method of the stub. The
* best way to avoid this is to make constructors as lightweight as possible,
- * deferring any initialisation which depends on other modules. As a last
- * resort, you can use StubObject::isRealObject() to break the loop, but as a
- * general rule, the stub object mechanism should be transparent, and code
+ * deferring any initialisation which depends on other modules. As a last
+ * resort, you can use StubObject::isRealObject() to break the loop, but as a
+ * general rule, the stub object mechanism should be transparent, and code
* which refers to it should be kept to a minimum.
*/
class StubObject {
var $mGlobal, $mClass, $mParams;
+
+ /**
+ * Constructor.
+ *
+ * @param String $global name of the global variable.
+ * @param String $class name of the class of the real object.
+ * @param Array $param array of parameters to pass to contructor of the real
+ * object.
+ */
function __construct( $global = null, $class = null, $params = array() ) {
$this->mGlobal = $global;
$this->mClass = $class;
$this->mParams = $params;
}
+ /**
+ * Returns a bool value whetever $obj is a stub object. Can be used to break
+ * a infinite loop when unstubbing an object.
+ *
+ * @param Object $obj object to check.
+ * @return bool true if $obj is not an instance of StubObject class.
+ */
static function isRealObject( $obj ) {
return is_object( $obj ) && !($obj instanceof StubObject);
}
+ /**
+ * Function called if any function exists with that name in this object.
+ * It is used to unstub the object. Only used internally, PHP will call
+ * self::__call() function and that function will call this function.
+ * This function will also call the function with the same name in the real
+ * object.
+ *
+ * @param String $name name of the function called.
+ * @param Array $args array of arguments.
+ */
function _call( $name, $args ) {
$this->_unstub( $name, 5 );
return call_user_func_array( array( $GLOBALS[$this->mGlobal], $name ), $args );
}
+ /**
+ * Create a new object to replace this stub object.
+ */
function _newObject() {
return wfCreateObject( $this->mClass, $this->mParams );
}
+ /**
+ * Function called by PHP if no function with that name exists in this
+ * object.
+ *
+ * @param String $name name of the function called
+ * @param Array $args array of arguments
+ */
function __call( $name, $args ) {
return $this->_call( $name, $args );
}
/**
- * This is public, for the convenience of external callers wishing to access
+ * This function creates a new object of the real class and replace it in
+ * the global variable.
+ * This is public, for the convenience of external callers wishing to access
* properties, e.g. eval.php
+ *
+ * @param String $name name of the method called in this object.
+ * @param Integer $level level to go in the stact trace to get the function
+ * who called this function.
*/
function _unstub( $name = '_unstub', $level = 2 ) {
static $recursionLevel = 0;
@@ -61,13 +103,18 @@ class StubObject {
}
}
+/**
+ * Stub object for the content language of this wiki. This object have to be in
+ * $wgContLang global.
+ */
class StubContLang extends StubObject {
+
function __construct() {
parent::__construct( 'wgContLang' );
}
function __call( $name, $args ) {
- return StubObject::_call( $name, $args );
+ return $this->_call( $name, $args );
}
function _newObject() {
@@ -78,7 +125,14 @@ class StubContLang extends StubObject {
return $obj;
}
}
+
+/**
+ * Stub object for the user language. It depends of the user preferences and
+ * "uselang" parameter that can be passed to index.php. This object have to be
+ * in $wgLang global.
+ */
class StubUserLang extends StubObject {
+
function __construct() {
parent::__construct( 'wgLang' );
}
@@ -89,15 +143,15 @@ class StubUserLang extends StubObject {
function _newObject() {
global $wgContLanguageCode, $wgRequest, $wgUser, $wgContLang;
- $code = $wgRequest->getVal('uselang', $wgUser->getOption('language') );
+ $code = $wgRequest->getVal( 'uselang', $wgUser->getOption( 'language' ) );
// if variant is explicitely selected, use it instead the one from wgUser
// see bug #7605
- if($wgContLang->hasVariants()){
+ if( $wgContLang->hasVariants() && in_array($code, $wgContLang->getVariants()) ){
$variant = $wgContLang->getPreferredVariant();
- if($variant != $wgContLanguageCode)
+ if( $variant != $wgContLanguageCode )
$code = $variant;
- }
+ }
# Validate $code
if( empty( $code ) || !preg_match( '/^[a-z-]+$/', $code ) ) {
@@ -105,7 +159,7 @@ class StubUserLang extends StubObject {
$code = $wgContLanguageCode;
}
- if( $code === $wgContLanguageCode || !Language::localisationExist( $code ) ) {
+ if( $code === $wgContLanguageCode ) {
return $wgContLang;
} else {
$obj = Language::factory( $code );
@@ -113,7 +167,15 @@ class StubUserLang extends StubObject {
}
}
}
+
+/**
+ * Stub object for the user. The initialisation of the will depend of
+ * $wgCommandLineMode. If it's true, it will be an anonymous user and if it's
+ * false, the user will be loaded from credidentails provided by cookies. This
+ * object have to be in $wgUser global.
+ */
class StubUser extends StubObject {
+
function __construct() {
parent::__construct( 'wgUser' );
}
@@ -121,18 +183,14 @@ class StubUser extends StubObject {
function __call( $name, $args ) {
return $this->_call( $name, $args );
}
-
+
function _newObject() {
global $wgCommandLineMode;
if( $wgCommandLineMode ) {
$user = new User;
} else {
$user = User::newFromSession();
- wfRunHooks('AutoAuthenticate',array(&$user));
}
return $user;
}
}
-
-
-
diff --git a/includes/Title.php b/includes/Title.php
index 8a9d3eee..ad425c5e 100644
--- a/includes/Title.php
+++ b/includes/Title.php
@@ -1,7 +1,7 @@
<?php
/**
* See title.txt
- *
+ * @file
*/
/** */
@@ -33,8 +33,8 @@ class Title {
*/
static private $titleCache=array();
static private $interwikiCache=array();
-
-
+
+
/**
* All member variables should be considered private
* Please use the accessor functions
@@ -63,6 +63,8 @@ class Title {
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?
/**#@-*/
@@ -83,6 +85,8 @@ class Title {
$this->mWatched = NULL;
$this->mLatestID = false;
$this->mOldRestrictions = false;
+ $this->mLength = -1;
+ $this->mRedirect = NULL;
}
/**
@@ -188,12 +192,13 @@ class Title {
* 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
*/
- public static function newFromID( $id ) {
+ public static function newFromID( $id, $flags = 0 ) {
$fname = 'Title::newFromID';
- $dbr = wfGetDB( DB_SLAVE );
- $row = $dbr->selectRow( 'page', array( 'page_namespace', 'page_title' ),
+ $db = ($flags & GAID_FOR_UPDATE) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
+ $row = $db->selectRow( 'page', array( 'page_namespace', 'page_title' ),
array( 'page_id' => $id ), $fname );
if ( $row !== false ) {
$title = Title::makeTitle( $row->page_namespace, $row->page_title );
@@ -204,7 +209,7 @@ class Title {
}
/**
- * Make an array of titles from an array of IDs
+ * Make an array of titles from an array of IDs
*/
public static function newFromIDs( $ids ) {
if ( !count( $ids ) ) {
@@ -222,6 +227,21 @@ class Title {
}
/**
+ * Make a Title object from a DB row
+ * @param Row $row (needs at least page_title,page_namespace)
+ */
+ public static function newFromRow( $row ) {
+ $t = self::makeTitle( $row->page_namespace, $row->page_title );
+
+ $t->mArticleID = isset($row->page_id) ? intval($row->page_id) : -1;
+ $t->mLength = isset($row->page_len) ? intval($row->page_len) : -1;
+ $t->mRedirect = isset($row->page_is_redirect) ? (bool)$row->page_is_redirect : NULL;
+ $t->mLatestID = isset($row->page_latest) ? $row->page_latest : false;
+
+ return $t;
+ }
+
+ /**
* Create a new Title from a namespace index and a DB key.
* It's assumed that $ns and $title are *valid*, for instance when
* they came directly from the database or a special page name.
@@ -230,12 +250,13 @@ class Title {
*
* @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
*/
- public static function &makeTitle( $ns, $title ) {
+ public static function &makeTitle( $ns, $title, $fragment = '' ) {
$t = new Title();
$t->mInterwiki = '';
- $t->mFragment = '';
+ $t->mFragment = $fragment;
$t->mNamespace = $ns = intval( $ns );
$t->mDbkeyform = str_replace( ' ', '_', $title );
$t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
@@ -251,11 +272,12 @@ class Title {
*
* @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
*/
- public static function makeTitleSafe( $ns, $title ) {
+ public static function makeTitleSafe( $ns, $title, $fragment = '' ) {
$t = new Title();
- $t->mDbkeyform = Title::makeName( $ns, $title );
+ $t->mDbkeyform = Title::makeName( $ns, $title, $fragment );
if( $t->secureAndSplit() ) {
return $t;
} else {
@@ -285,10 +307,10 @@ class Title {
*/
public static function newFromRedirect( $text ) {
$redir = MagicWord::get( 'redirect' );
- if( $redir->matchStart( $text ) ) {
+ if( $redir->matchStart( trim($text) ) ) {
// Extract the first link and see if it's usable
$m = array();
- if( preg_match( '!\[{2}(.*?)(?:\||\]{2})!', $text, $m ) ) {
+ if( preg_match( '!\[{2}(.*?)(?:\|.*?)?\]{2}!', $text, $m ) ) {
// Strip preceding colon used to "escape" categories, etc.
// and URL-decode links
if( strpos( $m[1], '%' ) !== false ) {
@@ -371,13 +393,18 @@ 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
*/
- public static function makeName( $ns, $title ) {
+ public static function makeName( $ns, $title, $fragment = '' ) {
global $wgContLang;
- $n = $wgContLang->getNsText( $ns );
- return $n == '' ? $title : "$n:$title";
+ $namespace = $wgContLang->getNsText( $ns );
+ $name = $namespace == '' ? $title : "$namespace:$title";
+ if ( strval( $fragment ) != '' ) {
+ $name .= '#' . $fragment;
+ }
+ return $name;
}
/**
@@ -431,7 +458,7 @@ class Title {
return $s->iw_url;
}
-
+
/**
* Fetch interwiki prefix data from local cache in constant database
*
@@ -577,7 +604,7 @@ class Title {
*/
public function getSubjectNsText() {
global $wgContLang;
- return $wgContLang->getNsText( Namespace::getSubject( $this->mNamespace ) );
+ return $wgContLang->getNsText( MWNamespace::getSubject( $this->mNamespace ) );
}
/**
@@ -586,7 +613,7 @@ class Title {
*/
public function getTalkNsText() {
global $wgContLang;
- return( $wgContLang->getNsText( Namespace::getTalk( $this->mNamespace ) ) );
+ return( $wgContLang->getNsText( MWNamespace::getTalk( $this->mNamespace ) ) );
}
/**
@@ -594,7 +621,7 @@ class Title {
* @return bool
*/
public function canTalk() {
- return( Namespace::canTalk( $this->mNamespace ) );
+ return( MWNamespace::canTalk( $this->mNamespace ) );
}
/**
@@ -677,16 +704,15 @@ class Title {
* @return string Base name
*/
public function getBaseText() {
- global $wgNamespacesWithSubpages;
- if( !empty( $wgNamespacesWithSubpages[$this->mNamespace] ) ) {
- $parts = explode( '/', $this->getText() );
- # Don't discard the real title if there's no subpage involved
- if( count( $parts ) > 1 )
- unset( $parts[ count( $parts ) - 1 ] );
- return implode( '/', $parts );
- } else {
+ if( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
return $this->getText();
}
+
+ $parts = explode( '/', $this->getText() );
+ # Don't discard the real title if there's no subpage involved
+ if( count( $parts ) > 1 )
+ unset( $parts[ count( $parts ) - 1 ] );
+ return implode( '/', $parts );
}
/**
@@ -694,13 +720,11 @@ class Title {
* @return string Subpage name
*/
public function getSubpageText() {
- global $wgNamespacesWithSubpages;
- if( isset( $wgNamespacesWithSubpages[ $this->mNamespace ] ) && $wgNamespacesWithSubpages[ $this->mNamespace ] ) {
- $parts = explode( '/', $this->mTextform );
- return( $parts[ count( $parts ) - 1 ] );
- } else {
+ if( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
return( $this->mTextform );
}
+ $parts = explode( '/', $this->mTextform );
+ return( $parts[ count( $parts ) - 1 ] );
}
/**
@@ -835,7 +859,7 @@ class Title {
$url = "{$wgScript}?title={$dbkey}&{$query}";
}
}
-
+
// FIXME: this causes breakage in various places when we
// actually expected a local URL and end up with dupe prefixes.
if ($wgRequest->getVal('action') == 'render') {
@@ -942,27 +966,20 @@ class Title {
* @return boolean
*/
public function isProtected( $action = '' ) {
- global $wgRestrictionLevels;
+ global $wgRestrictionLevels, $wgRestrictionTypes;
# Special pages have inherent protection
if( $this->getNamespace() == NS_SPECIAL )
return true;
- # Check regular protection levels
- if( $action == 'edit' || $action == '' ) {
- $r = $this->getRestrictions( 'edit' );
- foreach( $wgRestrictionLevels as $level ) {
- if( in_array( $level, $r ) && $level != '' ) {
- return( true );
- }
- }
- }
-
- if( $action == 'move' || $action == '' ) {
- $r = $this->getRestrictions( 'move' );
- foreach( $wgRestrictionLevels as $level ) {
- if( in_array( $level, $r ) && $level != '' ) {
- return( true );
+ # Check regular protection levels
+ foreach( $wgRestrictionTypes as $type ){
+ if( $action == $type || $action == '' ) {
+ $r = $this->getRestrictions( $type );
+ foreach( $wgRestrictionLevels as $level ) {
+ if( in_array( $level, $r ) && $level != '' ) {
+ return true;
+ }
}
}
}
@@ -971,7 +988,7 @@ class Title {
}
/**
- * Is $wgUser is watching this page?
+ * Is $wgUser watching this page?
* @return boolean
*/
public function userIsWatching() {
@@ -1037,21 +1054,29 @@ 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.
*/
- public function getUserPermissionsErrors( $action, $user, $doExpensiveQueries = true ) {
+ public function getUserPermissionsErrors( $action, $user, $doExpensiveQueries = true, $ignoreErrors = array() ) {
+ if( !StubObject::isRealObject( $user ) ) {
+ //Since StubObject is always used on globals, we can unstub $wgUser here and set $user = $wgUser
+ global $wgUser;
+ $wgUser->_unstub( '', 5 );
+ $user = $wgUser;
+ }
$errors = $this->getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries );
global $wgContLang;
global $wgLang;
- global $wgEmailConfirmToEdit, $wgUser;
+ global $wgEmailConfirmToEdit;
- if ( $wgEmailConfirmToEdit && !$user->isEmailConfirmed() ) {
+ if ( $wgEmailConfirmToEdit && !$user->isEmailConfirmed() && $action != 'createaccount' ) {
$errors[] = array( 'confirmedittext' );
}
- if ( $user->isBlockedFrom( $this ) ) {
+ if ( $user->isBlockedFrom( $this ) && $action != 'createaccount' ) {
$block = $user->mBlock;
// This is from OutputPage::blockedPage
@@ -1096,7 +1121,18 @@ class Title {
$intended = $user->mBlock->mAddress;
- $errors[] = array ( ($block->mAuto ? 'autoblockedtext' : 'blockedtext'), $link, $reason, $ip, $name, $blockid, $blockExpiry, $intended, $blockTimestamp );
+ $errors[] = array( ($block->mAuto ? 'autoblockedtext' : 'blockedtext'), $link, $reason, $ip, $name,
+ $blockid, $blockExpiry, $intended, $blockTimestamp );
+ }
+
+ // Remove the errors being ignored.
+
+ foreach( $errors as $index => $error ) {
+ $error_key = is_array($error) ? $error[0] : $error;
+
+ if (in_array( $error_key, $ignoreErrors )) {
+ unset($errors[$index]);
+ }
}
return $errors;
@@ -1108,6 +1144,7 @@ class Title {
* 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.
*/
@@ -1118,6 +1155,7 @@ class Title {
// Use getUserPermissionsErrors instead
if ( !wfRunHooks( 'userCan', array( &$this, &$user, $action, &$result ) ) ) {
+ wfProfileOut( __METHOD__ );
return $result ? array() : array( array( 'badaccess-group0' ) );
}
@@ -1141,17 +1179,18 @@ class Title {
else if ($result === false )
$errors[] = array('badaccess-group0'); # a generic "We don't want them to do that"
}
-
- if( NS_SPECIAL == $this->mNamespace ) {
+
+ $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')
+ $errors[] = (NS_MEDIAWIKI == $this->mNamespace
+ ? array('protectedinterface')
: array( 'namespaceprotected', $ns ) );
}
@@ -1168,7 +1207,7 @@ class Title {
&& !preg_match('/^'.preg_quote($user->getName(), '/').'\//', $this->mTextform) ) {
$errors[] = array('customcssjsprotected');
}
-
+
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
@@ -1191,14 +1230,25 @@ class Title {
}
}
}
-
+
foreach( $this->getRestrictions($action) as $right ) {
// Backwards compatibility, rewrite sysop -> protect
if ( $right == 'sysop' ) {
$right = 'protect';
}
if( '' != $right && !$user->isAllowed( $right ) ) {
- $errors[] = array( 'protectedpagetext', $right );
+ //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) {
+ $errors[] = array( 'protectedpagetext', $right );
+ } else {
+ //Nothing, user can edit!
+ }
+ } else {
+ $errors[] = array( 'protectedpagetext', $right );
+ }
}
}
@@ -1208,7 +1258,7 @@ class Title {
}
}
- if ($action == 'create') {
+ if ($action == 'create') {
$title_protection = $this->getTitleProtection();
if (is_array($title_protection)) {
@@ -1273,7 +1323,7 @@ class Title {
}
$dbr = wfGetDB( DB_SLAVE );
- $res = $dbr->select( 'protected_titles', '*',
+ $res = $dbr->select( 'protected_titles', '*',
array ('pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey()) );
if ($row = $dbr->fetchRow( $res )) {
@@ -1333,7 +1383,7 @@ class Title {
public function deleteTitleProtection() {
$dbw = wfGetDB( DB_MASTER );
- $dbw->delete( 'protected_titles',
+ $dbw->delete( 'protected_titles',
array ('pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey()), __METHOD__ );
}
@@ -1371,7 +1421,7 @@ class Title {
* @return boolean
*/
public function isMovable() {
- return Namespace::isMovable( $this->getNamespace() )
+ return MWNamespace::isMovable( $this->getNamespace() )
&& $this->getInterwiki() == '';
}
@@ -1382,23 +1432,23 @@ class Title {
*/
public function userCanRead() {
global $wgUser, $wgGroupPermissions;
-
- # Shortcut for public wikis, allows skipping quite a bit of code path
- if ($wgGroupPermissions['*']['read'])
- return true;
-
+
$result = null;
wfRunHooks( 'userCan', array( &$this, &$wgUser, 'read', &$result ) );
if ( $result !== null ) {
return $result;
}
+ # Shortcut for public wikis, allows skipping quite a bit of code
+ if ($wgGroupPermissions['*']['read'])
+ return true;
+
if( $wgUser->isAllowed( 'read' ) ) {
return true;
} else {
global $wgWhitelistRead;
- /**
+ /**
* Always grant access to the login page.
* Even anons need to be able to log in.
*/
@@ -1412,14 +1462,16 @@ class Title {
if( !is_array($wgWhitelistRead) ) {
return false;
}
-
+
/**
* Check for explicit whitelisting
*/
$name = $this->getPrefixedText();
- if( in_array( $name, $wgWhitelistRead, true ) )
+ $dbName = $this->getPrefixedDBKey();
+ // Check with and without underscores
+ if( in_array($name,$wgWhitelistRead,true) || in_array($dbName,$wgWhitelistRead,true) )
return true;
-
+
/**
* Old settings might have the title prefixed with
* a colon for main-namespace pages
@@ -1428,7 +1480,7 @@ class Title {
if( in_array( ':' . $name, $wgWhitelistRead ) )
return true;
}
-
+
/**
* If it's a special page, ditch the subpage bit
* and check again
@@ -1455,7 +1507,7 @@ class Title {
* @return bool
*/
public function isTalkPage() {
- return Namespace::isTalk( $this->getNamespace() );
+ return MWNamespace::isTalk( $this->getNamespace() );
}
/**
@@ -1463,15 +1515,37 @@ class Title {
* @return bool
*/
public function isSubpage() {
- global $wgNamespacesWithSubpages;
-
- if( isset( $wgNamespacesWithSubpages[ $this->mNamespace ] ) ) {
- return ( strpos( $this->getText(), '/' ) !== false && $wgNamespacesWithSubpages[ $this->mNamespace ] == true );
- } else {
+ return MWNamespace::hasSubpages( $this->mNamespace )
+ ? strpos( $this->getText(), '/' ) !== false
+ : false;
+ }
+
+ /**
+ * Does this have subpages? (Warning, usually requires an extra DB query.)
+ * @return bool
+ */
+ public function hasSubpages() {
+ if( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
+ # Duh
return false;
}
+
+ # We dynamically add a member variable for the purpose of this method
+ # alone to cache the result. There's no point in having it hanging
+ # around uninitialized in every Title object; therefore we only add it
+ # if needed and don't declare it statically.
+ if( isset( $this->mHasSubpages ) ) {
+ return $this->mHasSubpages;
+ }
+
+ $db = wfGetDB( DB_SLAVE );
+ return $this->mHasSubpages = (bool)$db->selectField( 'page', '1',
+ "page_namespace = {$this->mNamespace} AND page_title LIKE '"
+ . $db->escapeLike( $this->mDbkeyform ) . "/%'",
+ __METHOD__
+ );
}
-
+
/**
* Could this page contain custom CSS or JavaScript, based
* on the title?
@@ -1515,14 +1589,14 @@ class Title {
* @return bool
*/
public function isCssSubpage() {
- return ( NS_USER == $this->mNamespace and preg_match("/\\/.*\\.css$/", $this->mTextform ) );
+ return ( NS_USER == $this->mNamespace && preg_match("/\\/.*\\.css$/", $this->mTextform ) );
}
/**
* Is this a .js subpage of a user page?
* @return bool
*/
public function isJsSubpage() {
- return ( NS_USER == $this->mNamespace and preg_match("/\\/.*\\.js$/", $this->mTextform ) );
+ return ( NS_USER == $this->mNamespace && preg_match("/\\/.*\\.js$/", $this->mTextform ) );
}
/**
* Protect css/js subpages of user pages: can $wgUser edit
@@ -1533,7 +1607,7 @@ class Title {
*/
public function userCanEditCssJsSubpage() {
global $wgUser;
- return ( $wgUser->isAllowed('editusercssjs') or preg_match('/^'.preg_quote($wgUser->getName(), '/').'\//', $this->mTextform) );
+ return ( $wgUser->isAllowed('editusercssjs') || preg_match('/^'.preg_quote($wgUser->getName(), '/').'\//', $this->mTextform) );
}
/**
@@ -1555,16 +1629,13 @@ class Title {
* 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 $wgEnableCascadingProtection, $wgRestrictionTypes;
+ global $wgRestrictionTypes;
# Define our dimension of restrictions types
$pagerestrictions = array();
foreach( $wgRestrictionTypes as $action )
$pagerestrictions[$action] = array();
- if (!$wgEnableCascadingProtection)
- return array( false, $pagerestrictions );
-
if ( isset( $this->mCascadeSources ) && $get_pages ) {
return array( $this->mCascadeSources, $this->mCascadingRestrictions );
} else if ( isset( $this->mHasCascadingRestrictions ) && !$get_pages ) {
@@ -1603,7 +1674,7 @@ class Title {
$sources = $get_pages ? array() : false;
$now = wfTimestampNow();
$purgeExpired = false;
-
+
while( $row = $dbr->fetchObject( $res ) ) {
$expiry = Block::decodeExpiry( $row->pr_expiry );
if( $expiry > $now ) {
@@ -1654,15 +1725,21 @@ class Title {
* @param resource $res restrictions as an SQL result.
*/
private function loadRestrictionsFromRow( $res, $oldFashionedRestrictions = NULL ) {
- $dbr = wfGetDb( DB_SLAVE );
+ global $wgRestrictionTypes;
+ $dbr = wfGetDB( DB_SLAVE );
+
+ foreach( $wgRestrictionTypes as $type ){
+ $this->mRestrictions[$type] = array();
+ }
- $this->mRestrictions['edit'] = array();
- $this->mRestrictions['move'] = array();
+ $this->mCascadeRestriction = false;
+ $this->mRestrictionsExpiry = Block::decodeExpiry('');
# Backwards-compatibility: also load the restrictions from the page record (old format).
- if ( $oldFashionedRestrictions == NULL ) {
- $oldFashionedRestrictions = $dbr->selectField( 'page', 'page_restrictions', array( 'page_id' => $this->getArticleId() ), __METHOD__ );
+ if ( $oldFashionedRestrictions === NULL ) {
+ $oldFashionedRestrictions = $dbr->selectField( 'page', 'page_restrictions',
+ array( 'page_id' => $this->getArticleId() ), __METHOD__ );
}
if ($oldFashionedRestrictions != '') {
@@ -1671,16 +1748,14 @@ class Title {
$temp = explode( '=', trim( $restrict ) );
if(count($temp) == 1) {
// old old format should be treated as edit/move restriction
- $this->mRestrictions["edit"] = explode( ',', trim( $temp[0] ) );
- $this->mRestrictions["move"] = explode( ',', trim( $temp[0] ) );
+ $this->mRestrictions['edit'] = explode( ',', trim( $temp[0] ) );
+ $this->mRestrictions['move'] = explode( ',', trim( $temp[0] ) );
} else {
$this->mRestrictions[$temp[0]] = explode( ',', trim( $temp[1] ) );
}
}
$this->mOldRestrictions = true;
- $this->mCascadeRestriction = false;
- $this->mRestrictionsExpiry = Block::decodeExpiry('');
}
@@ -1692,6 +1767,10 @@ class Title {
while ($row = $dbr->fetchObject( $res ) ) {
# Cycle through all the restrictions.
+ // Don't take care of restrictions types that aren't in $wgRestrictionTypes
+ if( !in_array( $row->pr_type, $wgRestrictionTypes ) )
+ continue;
+
// This code should be refactored, now that it's being used more generally,
// But I don't really see any harm in leaving it in Block for now -werdna
$expiry = Block::decodeExpiry( $row->pr_expiry );
@@ -1741,6 +1820,8 @@ class Title {
} else { // Get rid of the old restrictions
Title::purgeExpiredRestrictions();
}
+ } else {
+ $this->mRestrictionsExpiry = Block::decodeExpiry('');
}
$this->mRestrictionsLoaded = true;
}
@@ -1804,7 +1885,7 @@ class Title {
* @return int the ID
*/
public function getArticleID( $flags = 0 ) {
- $linkCache =& LinkCache::singleton();
+ $linkCache = LinkCache::singleton();
if ( $flags & GAID_FOR_UPDATE ) {
$oldUpdate = $linkCache->forUpdate( true );
$this->mArticleID = $linkCache->addLinkObj( $this );
@@ -1817,14 +1898,59 @@ class Title {
return $this->mArticleID;
}
- public function getLatestRevID() {
+ /**
+ * 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
+ */
+ 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;
+ }
+ $linkCache = LinkCache::singleton();
+ $this->mRedirect = (bool)$linkCache->getGoodLinkFieldObj( $this, 'redirect' );
+
+ return $this->mRedirect;
+ }
+
+ /**
+ * 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
+ */
+ 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;
+ }
+ $linkCache = LinkCache::singleton();
+ $this->mLength = intval( $linkCache->getGoodLinkFieldObj( $this, 'length' ) );
+
+ return $this->mLength;
+ }
+
+ /**
+ * 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
+ */
+ public function getLatestRevID( $flags = 0 ) {
if ($this->mLatestID !== false)
return $this->mLatestID;
- $db = wfGetDB(DB_SLAVE);
+ $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()),
+ array('rev_page' => $this->getArticleID($flags)),
'Title::getLatestRevID' );
}
@@ -1839,7 +1965,7 @@ class Title {
* @param int $newid the new Article ID
*/
public function resetArticleID( $newid ) {
- $linkCache =& LinkCache::singleton();
+ $linkCache = LinkCache::singleton();
$linkCache->clearBadLink( $this->getPrefixedDBkey() );
if ( 0 == $newid ) { $this->mArticleID = -1; }
@@ -1928,7 +2054,7 @@ class Title {
$this->mInterwiki = $this->mFragment = '';
$this->mNamespace = $this->mDefaultNamespace; # Usually NS_MAIN
-
+
$dbkey = $this->mDbkeyform;
# Strip Unicode bidi override characters.
@@ -1936,7 +2062,7 @@ class Title {
# 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
-
+
# Clean up whitespace
#
$dbkey = preg_replace( '/[ _]+/', '_', $dbkey );
@@ -2043,7 +2169,7 @@ class Title {
{
return false;
}
-
+
/**
* Magic tilde sequences? Nu-uh!
*/
@@ -2055,11 +2181,11 @@ class Title {
* Limit the size of titles to 255 bytes.
* This is typically the size of the underlying database field.
* We make an exception for special pages, which don't need to be stored
- * in the database, and may edge over 255 bytes due to subpage syntax
+ * in the database, and may edge over 255 bytes due to subpage syntax
* for long titles, e.g. [[Special:Block/Long name]]
*/
if ( ( $this->mNamespace != NS_SPECIAL && strlen( $dbkey ) > 255 ) ||
- strlen( $dbkey ) > 512 )
+ strlen( $dbkey ) > 512 )
{
return false;
}
@@ -2088,18 +2214,18 @@ class Title {
return false;
}
// Allow IPv6 usernames to start with '::' by canonicalizing IPv6 titles.
- // IP names are not allowed for accounts, and can only be referring to
- // edits from the IP. Given '::' abbreviations and caps/lowercaps,
- // there are numerous ways to present the same IP. Having sp:contribs scan
- // them all is silly and having some show the edits and others not is
+ // IP names are not allowed for accounts, and can only be referring to
+ // edits from the IP. Given '::' abbreviations and caps/lowercaps,
+ // there are numerous ways to present the same IP. Having sp:contribs scan
+ // them all is silly and having some show the edits and others not is
// inconsistent. Same for talk/userpages. Keep them normalized instead.
- $dbkey = ($this->mNamespace == NS_USER || $this->mNamespace == NS_USER_TALK) ?
+ $dbkey = ($this->mNamespace == NS_USER || $this->mNamespace == NS_USER_TALK) ?
IP::sanitizeIP( $dbkey ) : $dbkey;
// Any remaining initial :s are illegal.
if ( $dbkey !== '' && ':' == $dbkey{0} ) {
return false;
}
-
+
# Fill fields
$this->mDbkeyform = $dbkey;
$this->mUrlform = wfUrlencode( $dbkey );
@@ -2112,7 +2238,7 @@ 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
+ * 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.
*
* @param string $fragment text
@@ -2127,7 +2253,7 @@ class Title {
* @return Title the object for the talk page
*/
public function getTalkPage() {
- return Title::makeTitle( Namespace::getTalk( $this->getNamespace() ), $this->getDBkey() );
+ return Title::makeTitle( MWNamespace::getTalk( $this->getNamespace() ), $this->getDBkey() );
}
/**
@@ -2137,7 +2263,7 @@ class Title {
* @return Title the object for the subject page
*/
public function getSubjectPage() {
- return Title::makeTitle( Namespace::getSubject( $this->getNamespace() ), $this->getDBkey() );
+ return Title::makeTitle( MWNamespace::getSubject( $this->getNamespace() ), $this->getDBkey() );
}
/**
@@ -2151,7 +2277,7 @@ class Title {
* @return array the Title objects linking here
*/
public function getLinksTo( $options = '', $table = 'pagelinks', $prefix = 'pl' ) {
- $linkCache =& LinkCache::singleton();
+ $linkCache = LinkCache::singleton();
if ( $options ) {
$db = wfGetDB( DB_MASTER );
@@ -2160,7 +2286,7 @@ class Title {
}
$res = $db->select( array( 'page', $table ),
- array( 'page_namespace', 'page_title', 'page_id' ),
+ array( 'page_namespace', 'page_title', 'page_id', 'page_len', 'page_is_redirect' ),
array(
"{$prefix}_from=page_id",
"{$prefix}_namespace" => $this->getNamespace(),
@@ -2172,7 +2298,7 @@ class Title {
if ( $db->numRows( $res ) ) {
while ( $row = $db->fetchObject( $res ) ) {
if ( $titleObj = Title::makeTitle( $row->page_namespace, $row->page_title ) ) {
- $linkCache->addGoodLinkObj( $row->page_id, $titleObj );
+ $linkCache->addGoodLinkObj( $row->page_id, $titleObj, $row->page_len, $row->page_is_redirect );
$retVal[] = $titleObj;
}
}
@@ -2284,50 +2410,68 @@ class Title {
/**
* Check whether a given move operation would be valid.
- * Returns true if ok, or a message key string for an error message
- * if invalid. (Scarrrrry ugly interface this.)
+ * Returns true if ok, or a getUserPermissionsErrors()-like array otherwise
* @param Title &$nt the new title
* @param bool $auth indicates whether $wgUser's permissions
* should be checked
- * @return mixed true on success, message name on failure
+ * @param string $reason is the log summary of the move, used for spam checking
+ * @return mixed True on success, getUserPermissionsErrors()-like array on failure
*/
- public function isValidMoveOperation( &$nt, $auth = true ) {
- if( !$this or !$nt ) {
- return 'badtitletext';
+ public function isValidMoveOperation( &$nt, $auth = true, $reason = '' ) {
+ $errors = array();
+ if( !$nt ) {
+ // Normally we'd add this to $errors, but we'll get
+ // lots of syntax errors if $nt is not an object
+ return array(array('badtitletext'));
}
if( $this->equals( $nt ) ) {
- return 'selfmove';
+ $errors[] = array('selfmove');
}
if( !$this->isMovable() || !$nt->isMovable() ) {
- return 'immobile_namespace';
+ $errors[] = array('immobile_namespace');
}
$oldid = $this->getArticleID();
$newid = $nt->getArticleID();
if ( strlen( $nt->getDBkey() ) < 1 ) {
- return 'articleexists';
+ $errors[] = array('articleexists');
}
if ( ( '' == $this->getDBkey() ) ||
( !$oldid ) ||
( '' == $nt->getDBkey() ) ) {
- return 'badarticleerror';
+ $errors[] = array('badarticleerror');
+ }
+
+ // Image-specific checks
+ if( $this->getNamespace() == NS_IMAGE ) {
+ $file = wfLocalFile( $this );
+ if( $file->exists() ) {
+ if( $nt->getNamespace() != NS_IMAGE ) {
+ $errors[] = array('imagenocrossnamespace');
+ }
+ if( $nt->getText() != wfStripIllegalFilenameChars( $nt->getText() ) ) {
+ $errors[] = array('imageinvalidfilename');
+ }
+ if( !File::checkExtensionCompatibility( $file, $nt->getDbKey() ) ) {
+ $errors[] = array('imagetypemismatch');
+ }
+ }
}
if ( $auth ) {
global $wgUser;
- $errors = array_merge($this->getUserPermissionsErrors('move', $wgUser),
+ $errors = array_merge($errors,
+ $this->getUserPermissionsErrors('move', $wgUser),
$this->getUserPermissionsErrors('edit', $wgUser),
$nt->getUserPermissionsErrors('move', $wgUser),
$nt->getUserPermissionsErrors('edit', $wgUser));
- if($errors !== array())
- return $errors[0][0];
}
global $wgUser;
$err = null;
- if( !wfRunHooks( 'AbortMove', array( $this, $nt, $wgUser, &$err ) ) ) {
- return 'hookaborted';
+ if( !wfRunHooks( 'AbortMove', array( $this, $nt, $wgUser, &$err, $reason ) ) ) {
+ $errors[] = array('hookaborted', $err);
}
# The move is allowed only if (1) the target doesn't exist, or
@@ -2336,15 +2480,18 @@ class Title {
if ( 0 != $newid ) { # Target exists; check for validity
if ( ! $this->isValidMoveTarget( $nt ) ) {
- return 'articleexists';
+ $errors[] = array('articleexists');
}
} else {
$tp = $nt->getTitleProtection();
- if ( $tp and !$wgUser->isAllowed( $tp['pt_create_perm'] ) ) {
- return 'cantmove-titleprotected';
+ $right = ( $tp['pt_create_perm'] == 'sysop' ) ? 'protect' : $tp['pt_create_perm'];
+ if ( $tp and !$wgUser->isAllowed( $right ) ) {
+ $errors[] = array('cantmove-titleprotected');
}
}
- return true;
+ if(empty($errors))
+ return true;
+ return $errors;
}
/**
@@ -2355,22 +2502,26 @@ class 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.
* Ignored if the user doesn't have the suppressredirect right.
- * @return mixed true on success, message name on failure
+ * @return mixed true on success, getUserPermissionsErrors()-like array on failure
*/
public function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true ) {
- $err = $this->isValidMoveOperation( $nt, $auth );
- if( is_string( $err ) ) {
+ $err = $this->isValidMoveOperation( $nt, $auth, $reason );
+ if( is_array( $err ) ) {
return $err;
}
$pageid = $this->getArticleID();
if( $nt->exists() ) {
- $this->moveOverExistingRedirect( $nt, $reason, $createRedirect );
+ $err = $this->moveOverExistingRedirect( $nt, $reason, $createRedirect );
$pageCountChange = ($createRedirect ? 0 : -1);
} else { # Target didn't exist, do normal move.
- $this->moveToNewTitle( $nt, $reason, $createRedirect );
+ $err = $this->moveToNewTitle( $nt, $reason, $createRedirect );
$pageCountChange = ($createRedirect ? 1 : 0);
}
+
+ if( is_array( $err ) ) {
+ return $err;
+ }
$redirid = $this->getArticleID();
// Category memberships include a sort key which may be customized.
@@ -2437,7 +2588,7 @@ class Title {
$newarticle = new Article( $nt );
$wgMessageCache->replace( $nt->getDBkey(), $newarticle->getContent() );
}
-
+
global $wgUser;
wfRunHooks( 'TitleMoveComplete', array( &$this, &$nt, &$wgUser, $pageid, $redirid ) );
return true;
@@ -2465,7 +2616,9 @@ class Title {
$now = wfTimestampNow();
$newid = $nt->getArticleID();
$oldid = $this->getArticleID();
+
$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.
@@ -2489,6 +2642,9 @@ class Title {
# Save a null revision in the page's history notifying of the move
$nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true );
$nullRevId = $nullRevision->insertOn( $dbw );
+
+ $article = new Article( $this );
+ wfRunHooks( 'NewRevisionFromEditComplete', array($article, $nullRevision, false) );
# Change the name of the target page:
$dbw->update( 'page',
@@ -2504,8 +2660,7 @@ class Title {
$nt->resetArticleID( $oldid );
# Recreate the redirect, this time in the other direction.
- if($createRedirect || !$wgUser->isAllowed('suppressredirect'))
- {
+ if( $createRedirect || !$wgUser->isAllowed('suppressredirect') ) {
$mwRedir = MagicWord::get( 'redirect' );
$redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n";
$redirectArticle = new Article( $this );
@@ -2516,6 +2671,8 @@ class Title {
'text' => $redirectText ) );
$redirectRevision->insertOn( $dbw );
$redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
+
+ wfRunHooks( 'NewRevisionFromEditComplete', array($redirectArticle, $redirectRevision, false) );
# Now, we record the link from the redirect to the new title.
# It should have no other outgoing links...
@@ -2530,6 +2687,19 @@ class Title {
$this->resetArticleID( 0 );
}
+ # Move an image if this is a file
+ if( $this->getNamespace() == NS_IMAGE ) {
+ $file = wfLocalFile( $this );
+ if( $file->exists() ) {
+ $status = $file->move( $nt );
+ if( !$status->isOk() ) {
+ $dbw->rollback();
+ return $status->getErrorsArray();
+ }
+ }
+ }
+ $dbw->commit();
+
# Log the move
$log = new LogPage( 'move' );
$log->addEntry( 'move_redir', $this, $reason, array( 1 => $nt->getPrefixedText() ) );
@@ -2540,6 +2710,7 @@ class Title {
$u = new SquidUpdate( $urls );
$u->doUpdate();
}
+
}
/**
@@ -2559,12 +2730,17 @@ class Title {
$newid = $nt->getArticleID();
$oldid = $this->getArticleID();
+
$dbw = wfGetDB( DB_MASTER );
+ $dbw->begin();
$now = $dbw->timestamp();
# Save a null revision in the page's history notifying of the move
$nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true );
$nullRevId = $nullRevision->insertOn( $dbw );
+
+ $article = new Article( $this );
+ wfRunHooks( 'NewRevisionFromEditComplete', array($article, $nullRevision, false) );
# Rename page entry
$dbw->update( 'page',
@@ -2579,8 +2755,7 @@ class Title {
);
$nt->resetArticleID( $oldid );
- if($createRedirect || !$wgUser->isAllowed('suppressredirect'))
- {
+ if( $createRedirect || !$wgUser->isAllowed('suppressredirect') ) {
# Insert redirect
$mwRedir = MagicWord::get( 'redirect' );
$redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n";
@@ -2592,6 +2767,8 @@ class Title {
'text' => $redirectText ) );
$redirectRevision->insertOn( $dbw );
$redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
+
+ wfRunHooks( 'NewRevisionFromEditComplete', array($redirectArticle, $redirectRevision, false) );
# Record the just-created redirect's linking to the page
$dbw->insert( 'pagelinks',
@@ -2603,6 +2780,19 @@ class Title {
} else {
$this->resetArticleID( 0 );
}
+
+ # Move an image if this is a file
+ if( $this->getNamespace() == NS_IMAGE ) {
+ $file = wfLocalFile( $this );
+ if( $file->exists() ) {
+ $status = $file->move( $nt );
+ if( !$status->isOk() ) {
+ $dbw->rollback();
+ return $status->getErrorsArray();
+ }
+ }
+ }
+ $dbw->commit();
# Log the move
$log = new LogPage( 'move' );
@@ -2614,6 +2804,7 @@ class Title {
# Purge old title from squid
# The new title, and links to the new title, are purged in Article::onArticleCreate()
$this->purgeSquid();
+
}
/**
@@ -2627,6 +2818,15 @@ class Title {
$fname = 'Title::isValidMoveTarget';
$dbw = wfGetDB( DB_MASTER );
+ # Is it an existsing file?
+ if( $nt->getNamespace() == NS_IMAGE ) {
+ $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'),
@@ -2670,7 +2870,7 @@ class Title {
# Return true if there was no history
return $row === false;
}
-
+
/**
* Can this title be added to a user's watchlist?
*
@@ -2678,7 +2878,7 @@ class Title {
*/
public function isWatchable() {
return !$this->isExternal()
- && Namespace::isWatchable( $this->getNamespace() );
+ && MWNamespace::isWatchable( $this->getNamespace() );
}
/**
@@ -2701,13 +2901,13 @@ class Title {
." AND cl_from <> '0'"
." ORDER BY cl_sortkey";
- $res = $dbr->query ( $sql ) ;
+ $res = $dbr->query( $sql );
- if($dbr->numRows($res) > 0) {
- while ( $x = $dbr->fetchObject ( $res ) )
+ 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();
- $dbr->freeResult ( $res ) ;
+ $data[$wgContLang->getNSText( NS_CATEGORY ).':'.$x->cl_to] = $this->getFullText();
+ $dbr->freeResult( $res );
} else {
$data = array();
}
@@ -2720,10 +2920,11 @@ class Title {
* @return array
*/
public function getParentCategoryTree( $children = array() ) {
+ $stack = array();
$parents = $this->getParentCategories();
- if($parents != '') {
- foreach($parents as $parent => $current) {
+ if( $parents ) {
+ foreach( $parents as $parent => $current ) {
if ( array_key_exists( $parent, $children ) ) {
# Circular reference
$stack[$parent] = array();
@@ -2755,30 +2956,43 @@ class Title {
* 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
*/
- public function getPreviousRevisionID( $revision ) {
- $dbr = wfGetDB( DB_SLAVE );
- return $dbr->selectField( 'revision', 'rev_id',
- 'rev_page=' . intval( $this->getArticleId() ) .
- ' AND rev_id<' . intval( $revision ) . ' ORDER BY rev_id DESC' );
+ public function getPreviousRevisionID( $revision, $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 )
+ ),
+ __METHOD__,
+ array( 'ORDER BY' => 'rev_id DESC' )
+ );
}
/**
* 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
*/
- public function getNextRevisionID( $revision ) {
- $dbr = wfGetDB( DB_SLAVE );
- return $dbr->selectField( 'revision', 'rev_id',
- 'rev_page=' . intval( $this->getArticleId() ) .
- ' AND rev_id>' . intval( $revision ) . ' ORDER BY rev_id' );
+ public function getNextRevisionID( $revision, $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 )
+ ),
+ __METHOD__,
+ array( 'ORDER BY' => 'rev_id' )
+ );
}
/**
* 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.
@@ -2789,7 +3003,9 @@ class Title {
return $dbr->selectField( 'revision', 'count(*)',
'rev_page = ' . intval( $this->getArticleId() ) .
' AND rev_id > ' . intval( $old ) .
- ' AND rev_id < ' . intval( $new ) );
+ ' AND rev_id < ' . intval( $new ),
+ __METHOD__,
+ array( 'USE INDEX' => 'PRIMARY' ) );
}
/**
@@ -2804,7 +3020,18 @@ class Title {
&& $this->getNamespace() == $title->getNamespace()
&& $this->getDBkey() === $title->getDBkey();
}
-
+
+ /**
+ * Callback for usort() to do title sorts by (namespace, title)
+ */
+ static function compare( $a, $b ) {
+ if( $a->getNamespace() == $b->getNamespace() ) {
+ return strcmp( $a->getText(), $b->getText() );
+ } else {
+ return $a->getNamespace() - $b->getNamespace();
+ }
+ }
+
/**
* Return a string representation of this title
*
@@ -2842,7 +3069,7 @@ class Title {
/**
* Update page_touched timestamps and send squid purge messages for
- * pages linking to this title. May be sent to the job queue depending
+ * pages linking to this title. May be sent to the job queue depending
* on the number of links. Typically called on create and delete.
*/
public function touchLinks() {
@@ -2861,7 +3088,7 @@ class Title {
public function getTouched() {
$dbr = wfGetDB( DB_SLAVE );
$touched = $dbr->selectField( 'page', 'page_touched',
- array(
+ array(
'page_namespace' => $this->getNamespace(),
'page_title' => $this->getDBkey()
), __METHOD__
@@ -2881,7 +3108,12 @@ class Title {
$title = htmlspecialchars($this->getText());
$tburl = $this->trackbackURL();
- return "
+ // Autodiscovery RDF is placed in comments so HTML validator
+ // won't barf. This is a rather icky workaround, but seems
+ // frequently used by this kind of RDF thingy.
+ //
+ // Spec: http://www.sixapart.com/pronet/docs/trackback_spec
+ return "<!--
<rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"
xmlns:dc=\"http://purl.org/dc/elements/1.1/\"
xmlns:trackback=\"http://madskills.com/public/xml/rss/module/trackback/\">
@@ -2890,7 +3122,8 @@ class Title {
dc:identifier=\"$url\"
dc:title=\"$title\"
trackback:ping=\"$tburl\" />
-</rdf:RDF>";
+</rdf:RDF>
+-->";
}
/**
@@ -2948,7 +3181,7 @@ class Title {
}
/**
- * If the Title refers to a special page alias which is not the local default,
+ * 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.
*/
public function fixSpecialName() {
@@ -2972,7 +3205,31 @@ class Title {
* @return bool
*/
public function isContentPage() {
- return Namespace::isContent( $this->getNamespace() );
+ return MWNamespace::isContent( $this->getNamespace() );
+ }
+
+ public function getRedirectsHere( $ns = null ) {
+ $redirs = array();
+
+ $dbr = wfGetDB( DB_SLAVE );
+ $where = array(
+ 'rd_namespace' => $this->getNamespace(),
+ 'rd_title' => $this->getDBkey(),
+ 'rd_from = page_id'
+ );
+ if ( !is_null($ns) ) $where['page_namespace'] = $ns;
+
+ $result = $dbr->select(
+ array( 'redirect', 'page' ),
+ array( 'page_namespace', 'page_title' ),
+ $where,
+ __METHOD__
+ );
+
+
+ while( $row = $dbr->fetchObject( $result ) ) {
+ $redirs[] = self::newFromRow( $row );
+ }
+ return $redirs;
}
-
}
diff --git a/includes/User.php b/includes/User.php
index 8e3c776a..5c129819 100644
--- a/includes/User.php
+++ b/includes/User.php
@@ -1,21 +1,21 @@
<?php
/**
* See user.txt
- *
+ * @file
*/
# Number of characters in user_token field
define( 'USER_TOKEN_LENGTH', 32 );
# Serialized record version
-define( 'MW_USER_VERSION', 5 );
+define( 'MW_USER_VERSION', 6 );
# Some punctuation to prevent editing from broken text-mangling proxies.
define( 'EDIT_TOKEN_SUFFIX', '+\\' );
/**
* Thrown by User::setPassword() on error
- * @addtogroup Exception
+ * @ingroup Exception
*/
class PasswordError extends MWException {
// NOP
@@ -34,8 +34,8 @@ class PasswordError extends MWException {
class User {
/**
- * A list of default user toggles, i.e. boolean user preferences that are
- * displayed by Special:Preferences as checkboxes. This list can be
+ * A list of default user toggles, i.e. boolean user preferences that are
+ * displayed by Special:Preferences as checkboxes. This list can be
* extended via the UserToggles hook or $wgContLang->getExtraUserToggles().
*/
static public $mToggles = array(
@@ -76,11 +76,12 @@ class User {
'watchlisthideminor',
'ccmeonemails',
'diffonly',
+ 'showhiddencats',
);
/**
* List of member variables which are saved to the shared cache (memcached).
- * Any operation which changes the corresponding database fields must
+ * Any operation which changes the corresponding database fields must
* call a cache-clearing function.
*/
static $mCacheVars = array(
@@ -105,10 +106,57 @@ class User {
);
/**
+ * Core rights
+ * Each of these should have a corresponding message of the form "right-$right"
+ */
+ static $mCoreRights = array(
+ 'apihighlimits',
+ 'autoconfirmed',
+ 'autopatrol',
+ 'bigdelete',
+ 'block',
+ 'blockemail',
+ 'bot',
+ 'browsearchive',
+ 'createaccount',
+ 'createpage',
+ 'createtalk',
+ 'delete',
+ 'deletedhistory',
+ 'edit',
+ 'editinterface',
+ 'editusercssjs',
+ 'import',
+ 'importupload',
+ 'ipblock-exempt',
+ 'markbotedits',
+ 'minoredit',
+ 'move',
+ 'nominornewtalk',
+ 'noratelimit',
+ 'patrol',
+ 'protect',
+ 'proxyunbannable',
+ 'purge',
+ 'read',
+ 'reupload',
+ 'reupload-shared',
+ 'rollback',
+ 'suppressredirect',
+ 'trackback',
+ 'undelete',
+ 'unwatchedpages',
+ 'upload',
+ 'upload_by_url',
+ 'userrights',
+ );
+ static $mAllRights = false;
+
+ /**
* The cache variable declarations
*/
- var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime,
- $mEmail, $mOptions, $mTouched, $mToken, $mEmailAuthenticated,
+ var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime,
+ $mEmail, $mOptions, $mTouched, $mToken, $mEmailAuthenticated,
$mEmailToken, $mEmailTokenExpires, $mRegistration, $mGroups;
/**
@@ -133,7 +181,7 @@ class User {
var $mNewtalk, $mDatePreference, $mBlockedby, $mHash, $mSkin, $mRights,
$mBlockreason, $mBlock, $mEffectiveGroups;
- /**
+ /**
* Lightweight constructor for anonymous user
* Use the User::newFrom* factory functions for other kinds of users
*/
@@ -188,7 +236,7 @@ class User {
if ( $this->mId == 0 ) {
$this->loadDefaults();
return false;
- }
+ }
# Try cache
$key = wfMemcKey( 'user', 'id', $this->mId );
@@ -197,7 +245,7 @@ class User {
# Object is expired, load from DB
$data = false;
}
-
+
if ( !$data ) {
wfDebug( "Cache miss for user {$this->mId}\n" );
# Load from DB
@@ -205,7 +253,6 @@ class User {
# Can't load from ID, user is anonymous
return false;
}
-
$this->saveToCache();
} else {
wfDebug( "Got user {$this->mId} from cache\n" );
@@ -222,6 +269,7 @@ class User {
*/
function saveToCache() {
$this->load();
+ $this->loadGroups();
if ( $this->isAnon() ) {
// Anonymous users are uncached
return;
@@ -240,17 +288,16 @@ class User {
* Static factory method for creation from username.
*
* This is slightly less efficient than newFromId(), so use newFromId() if
- * you have both an ID and a name handy.
+ * you have both an ID and a name handy.
*
- * @param string $name Username, validated by Title:newFromText()
- * @param mixed $validate Validate username. Takes the same parameters as
- * User::getCanonicalName(), except that true is accepted as an alias
+ * @param $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
+ *
+ * @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.
- * @static
+ * a name, zero user ID and default settings.
*/
static function newFromName( $name, $validate = 'valid' ) {
if ( $validate === true ) {
@@ -282,9 +329,8 @@ class User {
*
* If the code is invalid or has expired, returns NULL.
*
- * @param string $code
+ * @param $code string
* @return User
- * @static
*/
static function newFromConfirmationCode( $code ) {
$dbr = wfGetDB( DB_SLAVE );
@@ -298,13 +344,12 @@ class User {
return null;
}
}
-
+
/**
* Create a new user object using data from session or cookies. If the
* login credentials are invalid, the result is an anonymous user.
*
* @return User
- * @static
*/
static function newFromSession() {
$user = new User;
@@ -313,10 +358,19 @@ class User {
}
/**
+ * Create a new user object from a user row.
+ * The row should have all fields from the user table in it.
+ */
+ static function newFromRow( $row ) {
+ $user = new User;
+ $user->loadFromRow( $row );
+ return $user;
+ }
+
+ /**
* Get username given an id.
- * @param integer $id Database user id
+ * @param $id Integer: database user id
* @return string Nickname of a user
- * @static
*/
static function whoIs( $id ) {
$dbr = wfGetDB( DB_SLAVE );
@@ -326,7 +380,7 @@ class User {
/**
* Get the real name of a user given their identifier
*
- * @param int $id Database user id
+ * @param $id Int: database user id
* @return string Real name of a user
*/
static function whoIsReal( $id ) {
@@ -336,7 +390,7 @@ class User {
/**
* Get database id given a user name
- * @param string $name Nickname of a user
+ * @param $name String: nickname of a user
* @return integer|null Database user id (null: if non existent
* @static
*/
@@ -361,47 +415,19 @@ class User {
*
* This function exists for username validation, in order to reject
* usernames which are similar in form to IP addresses. Strings such
- * as 300.300.300.300 will return true because it looks like an IP
+ * as 300.300.300.300 will return true because it looks like an IP
* address, despite not being strictly valid.
- *
+ *
* We match \d{1,3}\.\d{1,3}\.\d{1,3}\.xxx as an anonymous IP
* address because the usemod software would "cloak" anonymous IP
* addresses like this, if we allowed accounts like this to be created
* new users could get the old edits of these anonymous users.
*
- * @static
- * @param string $name Nickname of a user
+ * @param $name String: nickname of a user
* @return bool
*/
static function isIP( $name ) {
- return preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/',$name) || User::isIPv6($name);
- /*return preg_match("/^
- (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
- (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
- (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
- (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))
- $/x", $name);*/
- }
-
- /**
- * Check if $name is an IPv6 IP.
- */
- static function isIPv6($name) {
- /*
- * if it has any non-valid characters, it can't be a valid IPv6
- * address.
- */
- if (preg_match("/[^:a-fA-F0-9]/", $name))
- return false;
-
- $parts = explode(":", $name);
- if (count($parts) < 3)
- return false;
- foreach ($parts as $part) {
- if (!preg_match("/^[0-9a-fA-F]{0,4}$/", $part))
- return false;
- }
- return true;
+ return preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/',$name) || IP::isIPv6($name);
}
/**
@@ -412,9 +438,8 @@ class User {
* is longer than the maximum allowed username size or doesn't begin with
* a capital letter.
*
- * @param string $name
+ * @param $name string
* @return bool
- * @static
*/
static function isValidUserName( $name ) {
global $wgContLang, $wgMaxNameChars;
@@ -423,17 +448,23 @@ class User {
|| User::isIP( $name )
|| strpos( $name, '/' ) !== false
|| strlen( $name ) > $wgMaxNameChars
- || $name != $wgContLang->ucfirst( $name ) )
+ || $name != $wgContLang->ucfirst( $name ) ) {
+ wfDebugLog( 'username', __METHOD__ .
+ ": '$name' invalid due to empty, IP, slash, length, or lowercase" );
return false;
+ }
// Ensure that the name can't be misresolved as a different title,
// such as with extra namespace keys at the start.
$parsed = Title::newFromText( $name );
if( is_null( $parsed )
|| $parsed->getNamespace()
- || strcmp( $name, $parsed->getPrefixedText() ) )
+ || strcmp( $name, $parsed->getPrefixedText() ) ) {
+ wfDebugLog( 'username', __METHOD__ .
+ ": '$name' invalid due to ambiguous prefixes" );
return false;
-
+ }
+
// Check an additional blacklist of troublemaker characters.
// Should these be merged into the title char list?
$unicodeBlacklist = '/[' .
@@ -445,12 +476,14 @@ class User {
'\x{e000}-\x{f8ff}' . # private use
']/u';
if( preg_match( $unicodeBlacklist, $name ) ) {
+ wfDebugLog( 'username', __METHOD__ .
+ ": '$name' invalid due to blacklisted characters" );
return false;
}
-
+
return true;
}
-
+
/**
* Usernames which fail to pass this function will be blocked
* from user login and new account registrations, but may be used
@@ -459,19 +492,28 @@ class User {
* If an account already exists in this form, login will be blocked
* by a failure to pass this function.
*
- * @param string $name
+ * @param $name string
* @return bool
*/
static function isUsableName( $name ) {
global $wgReservedUsernames;
- return
- // Must be a valid username, obviously ;)
- self::isValidUserName( $name ) &&
-
- // Certain names may be reserved for batch processes.
- !in_array( $name, $wgReservedUsernames );
+ // Must be a valid username, obviously ;)
+ if ( !self::isValidUserName( $name ) ) {
+ return false;
+ }
+
+ // Certain names may be reserved for batch processes.
+ foreach ( $wgReservedUsernames as $reserved ) {
+ if ( substr( $reserved, 0, 4 ) == 'msg:' ) {
+ $reserved = wfMsgForContent( substr( $reserved, 4 ) );
+ }
+ if ( $reserved == $name ) {
+ return false;
+ }
+ }
+ return true;
}
-
+
/**
* Usernames which fail to pass this function will be blocked
* from new account registrations, but may be used internally
@@ -482,13 +524,13 @@ class User {
* rather than in isValidUserName() to avoid disrupting
* existing accounts.
*
- * @param string $name
+ * @param $name string
* @return bool
*/
static function isCreatableName( $name ) {
return
self::isUsableName( $name ) &&
-
+
// Registration-time character blacklisting...
strpos( $name, '@' ) === false;
}
@@ -496,7 +538,7 @@ class User {
/**
* Is the input a valid password for this user?
*
- * @param string $password Desired password
+ * @param $password String: desired password
* @return bool
*/
function isValidPassword( $password ) {
@@ -507,7 +549,7 @@ class User {
return $result;
if( $result === false )
return false;
-
+
// Password needs to be long enough, and can't be the same as the username
return strlen( $password ) >= $wgMinimalPasswordLength
&& $wgContLang->lc( $password ) !== $wgContLang->lc( $this->mName );
@@ -522,7 +564,7 @@ class User {
*
* @todo Check for RFC 2822 compilance (bug 959)
*
- * @param string $addr email address
+ * @param $addr String: email address
* @return bool
*/
public static function isValidEmailAddr( $addr ) {
@@ -535,14 +577,14 @@ class User {
}
/**
- * Given unvalidated user input, return a canonical username, or false if
+ * Given unvalidated user input, return a canonical username, or false if
* the username is invalid.
- * @param string $name
- * @param mixed $validate Type of validation to use:
- * 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
+ * @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
*/
static function getCanonicalName( $name, $validate = 'valid' ) {
# Force usernames to capital
@@ -594,10 +636,9 @@ class User {
* Count the number of edits of a user
*
* It should not be static and some day should be merged as proper member function / deprecated -- domas
- *
- * @param int $uid The user ID to check
+ *
+ * @param $uid Int: the user ID to check
* @return int
- * @static
*/
static function edits( $uid ) {
wfProfileIn( __METHOD__ );
@@ -634,7 +675,6 @@ class User {
* @todo hash random numbers to improve security, like generateToken()
*
* @return string
- * @static
*/
static function randomPassword() {
global $wgMinimalPasswordLength;
@@ -651,7 +691,7 @@ class User {
}
/**
- * Set cached properties to default. Note: this no longer clears
+ * Set cached properties to default. Note: this no longer clears
* uncached lazy-initialised properties. The constructor does that instead.
*
* @private
@@ -682,14 +722,17 @@ class User {
$this->mRegistration = wfTimestamp( TS_MW );
$this->mGroups = array();
+ wfRunHooks( 'UserLoadDefaults', array( $this, $name ) );
+
wfProfileOut( __METHOD__ );
}
-
+
/**
* Initialise php session
* @deprecated use wfSetupSession()
*/
function SetupSession() {
+ wfDeprecated( __METHOD__ );
wfSetupSession();
}
@@ -701,6 +744,12 @@ class User {
private function loadFromSession() {
global $wgMemc, $wgCookiePrefix;
+ $result = null;
+ wfRunHooks( 'UserLoadFromSession', array( $this, &$result ) );
+ if ( $result !== null ) {
+ return $result;
+ }
+
if ( isset( $_SESSION['wsUserID'] ) ) {
if ( 0 != $_SESSION['wsUserID'] ) {
$sId = $_SESSION['wsUserID'];
@@ -731,7 +780,7 @@ class User {
# Not a valid ID, loadFromId has switched the object to anon for us
return false;
}
-
+
if ( isset( $_SESSION['wsToken'] ) ) {
$passwordCorrect = $_SESSION['wsToken'] == $this->mToken;
$from = 'session';
@@ -755,11 +804,11 @@ class User {
return false;
}
}
-
+
/**
* Load user and user_group data from the database
* $this->mId must be set, this is how the user is identified.
- *
+ *
* @return true if the user exists, false if the user is anonymous
* @private
*/
@@ -778,23 +827,50 @@ class User {
if ( $s !== false ) {
# Initialise user table data
- $this->mName = $s->user_name;
- $this->mRealName = $s->user_real_name;
- $this->mPassword = $s->user_password;
- $this->mNewpassword = $s->user_newpassword;
- $this->mNewpassTime = wfTimestampOrNull( TS_MW, $s->user_newpass_time );
- $this->mEmail = $s->user_email;
- $this->decodeOptions( $s->user_options );
- $this->mTouched = wfTimestamp(TS_MW,$s->user_touched);
- $this->mToken = $s->user_token;
- $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $s->user_email_authenticated );
- $this->mEmailToken = $s->user_email_token;
- $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $s->user_email_token_expires );
- $this->mRegistration = wfTimestampOrNull( TS_MW, $s->user_registration );
- $this->mEditCount = $s->user_editcount;
+ $this->loadFromRow( $s );
+ $this->mGroups = null; // deferred
$this->getEditCount(); // revalidation for nulls
+ return true;
+ } else {
+ # Invalid user_id
+ $this->mId = 0;
+ $this->loadDefaults();
+ return false;
+ }
+ }
- # Load group data
+ /**
+ * Initialise the user object from a row from the user table
+ */
+ function loadFromRow( $row ) {
+ $this->mDataLoaded = true;
+
+ if ( isset( $row->user_id ) ) {
+ $this->mId = $row->user_id;
+ }
+ $this->mName = $row->user_name;
+ $this->mRealName = $row->user_real_name;
+ $this->mPassword = $row->user_password;
+ $this->mNewpassword = $row->user_newpassword;
+ $this->mNewpassTime = wfTimestampOrNull( TS_MW, $row->user_newpass_time );
+ $this->mEmail = $row->user_email;
+ $this->decodeOptions( $row->user_options );
+ $this->mTouched = wfTimestamp(TS_MW,$row->user_touched);
+ $this->mToken = $row->user_token;
+ $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated );
+ $this->mEmailToken = $row->user_email_token;
+ $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires );
+ $this->mRegistration = wfTimestampOrNull( TS_MW, $row->user_registration );
+ $this->mEditCount = $row->user_editcount;
+ }
+
+ /**
+ * Load the groups from the database if they aren't already loaded
+ * @private
+ */
+ function loadGroups() {
+ if ( is_null( $this->mGroups ) ) {
+ $dbr = wfGetDB( DB_MASTER );
$res = $dbr->select( 'user_groups',
array( 'ug_group' ),
array( 'ug_user' => $this->mId ),
@@ -803,19 +879,13 @@ class User {
while( $row = $dbr->fetchObject( $res ) ) {
$this->mGroups[] = $row->ug_group;
}
- return true;
- } else {
- # Invalid user_id
- $this->mId = 0;
- $this->loadDefaults();
- return false;
}
}
/**
- * Clear various cached data stored in this object.
- * @param string $reloadFrom Reload user and user_groups table data from a
- * given source. May be "name", "id", "defaults", "session" or false for
+ * Clear various cached data stored in this object.
+ * @param $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 ) {
@@ -838,7 +908,6 @@ class User {
* and add the default language variants.
* Not really private cause it's called by Language class
* @return array
- * @static
* @private
*/
static function getDefaultOptions() {
@@ -865,13 +934,11 @@ class User {
/**
* Get a given default option value.
*
- * @param string $opt
+ * @param $opt string
* @return string
- * @static
- * @public
*/
- function getDefaultOption( $opt ) {
- $defOpts = User::getDefaultOptions();
+ public static function getDefaultOption( $opt ) {
+ $defOpts = self::getDefaultOptions();
if( isset( $defOpts[$opt] ) ) {
return $defOpts[$opt];
} else {
@@ -894,9 +961,10 @@ class User {
/**
* Get blocking information
* @private
- * @param bool $bFromSlave Specify whether to check slave or master. To improve performance,
- * non-critical checks are done against slaves. Check when actually saving should be done against
- * master.
+ * @param $bFromSlave Bool: specify whether to check slave or master. To
+ * improve performance, non-critical checks are done
+ * against slaves. Check when actually saving should be
+ * done against master.
*/
function getBlockedStatus( $bFromSlave = true ) {
global $wgEnableSorbs, $wgProxyWhitelist;
@@ -909,7 +977,14 @@ class User {
wfProfileIn( __METHOD__ );
wfDebug( __METHOD__.": checking...\n" );
- $this->mBlockedby = 0;
+ // Initialize data...
+ // Otherwise something ends up stomping on $this->mBlockedby when
+ // things get lazy-loaded later, causing false positive block hits
+ // due to -1 !== 0. Probably session-related... Nothing should be
+ // overwriting mBlockedby, surely?
+ $this->load();
+
+ $this->mBlockedby = 0;
$this->mHideName = 0;
$ip = wfGetIP();
@@ -936,7 +1011,6 @@ class User {
# Proxy blocking
if ( !$this->isAllowed('proxyunbannable') && !in_array( $ip, $wgProxyWhitelist ) ) {
-
# Local list
if ( wfIsLocallyBlockedProxy( $ip ) ) {
$this->mBlockedby = wfMsg( 'proxyblocker' );
@@ -970,7 +1044,7 @@ class User {
$found = false;
$host = '';
-
+ // FIXME: IPv6 ???
$m = array();
if ( preg_match( '/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $ip, $m ) ) {
# Make hostname
@@ -1001,7 +1075,11 @@ class User {
*/
public function isPingLimitable() {
global $wgRateLimitsExcludedGroups;
- return array_intersect($this->getEffectiveGroups(), $wgRateLimitsExcludedGroups) == array();
+ if( array_intersect( $this->getEffectiveGroups(), $wgRateLimitsExcludedGroups ) ) {
+ // Deprecated, but kept for backwards-compatibility config
+ return false;
+ }
+ return !$this->isAllowed('noratelimit');
}
/**
@@ -1012,7 +1090,6 @@ class User {
* last-hit counters will be shared across wikis.
*
* @return bool true if a rate limiter was tripped
- * @public
*/
function pingLimiter( $action='edit' ) {
@@ -1038,13 +1115,14 @@ class User {
$keys = array();
$id = $this->getId();
$ip = wfGetIP();
+ $userLimit = false;
if( isset( $limits['anon'] ) && $id == 0 ) {
$keys[wfMemcKey( 'limiter', $action, 'anon' )] = $limits['anon'];
}
if( isset( $limits['user'] ) && $id != 0 ) {
- $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $limits['user'];
+ $userLimit = $limits['user'];
}
if( $this->isNewbie() ) {
if( isset( $limits['newbie'] ) && $id != 0 ) {
@@ -1059,6 +1137,20 @@ class User {
$keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
}
}
+ // Check for group-specific permissions
+ // If more than one group applies, use the group with the highest limit
+ foreach ( $this->getGroups() as $group ) {
+ if ( isset( $limits[$group] ) ) {
+ if ( $userLimit === false || $limits[$group] > $userLimit ) {
+ $userLimit = $limits[$group];
+ }
+ }
+ }
+ // Set the user limit key
+ if ( $userLimit !== false ) {
+ wfDebug( __METHOD__.": effective user limit: $userLimit\n" );
+ $keys[ wfMemcKey( 'limiter', $action, 'user', $id ) ] = $userLimit;
+ }
$triggered = false;
foreach( $keys as $key => $limit ) {
@@ -1137,7 +1229,7 @@ class User {
/**
* Get the user ID. Returns 0 if the user is anonymous or nonexistent.
*/
- function getID() {
+ function getId() {
if( $this->mId === null and $this->mName !== null
and User::isIP( $this->mName ) ) {
// Special case, we know the user is anonymous
@@ -1151,9 +1243,8 @@ class User {
/**
* Set the user and reload all fields according to that ID
- * @deprecated use User::newFromId()
*/
- function setID( $v ) {
+ function setId( $v ) {
$this->mId = $v;
$this->clearInstanceCache( 'id' );
}
@@ -1176,12 +1267,12 @@ class User {
}
/**
- * Set the user name.
+ * Set the user name.
*
- * This does not reload fields from the database according to the given
+ * This does not reload fields from the database according to the given
* name. Rather, it is used to create a temporary "nonexistent user" for
- * later addition to the database. It can also be used to set the IP
- * address for an anonymous user to something other than the current
+ * later addition to the database. It can also be used to set the IP
+ * address for an anonymous user to something other than the current
* remote IP.
*
* User::newFromName() has rougly the same function, when the named user
@@ -1195,7 +1286,6 @@ class User {
/**
* Return the title dbkey form of the name, for eg user pages.
* @return string
- * @public
*/
function getTitleKey() {
return str_replace( ' ', '_', $this->getName() );
@@ -1245,14 +1335,14 @@ class User {
return array(array("wiki" => wfWikiID(), "link" => $utp->getLocalURL()));
}
-
+
/**
- * Perform a user_newtalk check, uncached.
+ * Perform a user_newtalk check, uncached.
* Use getNewtalk for a cached check.
- *
- * @param string $field
- * @param mixed $id
- * @param bool $fromMaster True to fetch from the master, false for a slave
+ *
+ * @param $field string
+ * @param $id mixed
+ * @param $fromMaster Bool: true to fetch from the master, false for a slave
* @return bool
* @private
*/
@@ -1269,8 +1359,8 @@ class User {
/**
* Add or update the
- * @param string $field
- * @param mixed $id
+ * @param $field string
+ * @param $id mixed
* @private
*/
function updateNewtalk( $field, $id ) {
@@ -1290,8 +1380,8 @@ class User {
/**
* Clear the new messages flag for the given user
- * @param string $field
- * @param mixed $id
+ * @param $field string
+ * @param $id mixed
* @private
*/
function deleteNewtalk( $field, $id ) {
@@ -1310,7 +1400,7 @@ class User {
/**
* Update the 'You have new messages!' status.
- * @param bool $val
+ * @param $val bool
*/
function setNewtalk( $val ) {
if( wfReadOnly() ) {
@@ -1345,7 +1435,7 @@ class User {
$this->invalidateCache();
}
}
-
+
/**
* Generate a current or new-future timestamp to be stored in the
* user_touched field when we update things.
@@ -1354,7 +1444,7 @@ class User {
global $wgClockSkewFudge;
return wfTimestamp( TS_MW, time() + $wgClockSkewFudge );
}
-
+
/**
* Clear user data from memcached.
* Use after applying fun updates to the database; caller's
@@ -1378,13 +1468,13 @@ class User {
$this->load();
if( $this->mId ) {
$this->mTouched = self::newTouchedTimestamp();
-
+
$dbw = wfGetDB( DB_MASTER );
$dbw->update( 'user',
array( 'user_touched' => $dbw->timestamp( $this->mTouched ) ),
array( 'user_id' => $this->mId ),
__METHOD__ );
-
+
$this->clearSharedCache();
}
}
@@ -1395,18 +1485,6 @@ class User {
}
/**
- * Encrypt a password.
- * It can eventually salt a password.
- * @see User::addSalt()
- * @param string $p clear Password.
- * @return string Encrypted password.
- */
- function encryptPassword( $p ) {
- $this->load();
- return wfEncryptPassword( $this->mId, $p );
- }
-
- /**
* 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
@@ -1417,20 +1495,20 @@ class User {
* wipes it, so the account cannot be logged in until
* a new password is set, for instance via e-mail.
*
- * @param string $str
+ * @param $str string
* @throws PasswordError on failure
*/
function setPassword( $str ) {
global $wgAuth;
-
+
if( $str !== null ) {
if( !$wgAuth->allowPasswordChange() ) {
throw new PasswordError( wfMsg( 'password-change-forbidden' ) );
}
-
+
if( !$this->isValidPassword( $str ) ) {
global $wgMinimalPasswordLength;
- throw new PasswordError( wfMsg( 'passwordtooshort',
+ throw new PasswordError( wfMsgExt( 'passwordtooshort', array( 'parsemag' ),
$wgMinimalPasswordLength ) );
}
}
@@ -1438,7 +1516,7 @@ class User {
if( !$wgAuth->setPassword( $this, $str ) ) {
throw new PasswordError( wfMsg( 'externaldberror' ) );
}
-
+
$this->setInternalPassword( $str );
return true;
@@ -1448,21 +1526,27 @@ class User {
* Set the password and reset the random token no matter
* what.
*
- * @param string $str
+ * @param $str string
*/
function setInternalPassword( $str ) {
$this->load();
$this->setToken();
-
+
if( $str === null ) {
// Save an invalid hash...
$this->mPassword = '';
} else {
- $this->mPassword = $this->encryptPassword( $str );
+ $this->mPassword = self::crypt( $str );
}
$this->mNewpassword = '';
$this->mNewpassTime = null;
}
+
+ function getToken() {
+ $this->load();
+ return $this->mToken;
+ }
+
/**
* Set the random token (used for persistent authentication)
* Called from loadDefaults() among other places.
@@ -1496,7 +1580,7 @@ class User {
*/
function setNewpassword( $str, $throttle = true ) {
$this->load();
- $this->mNewpassword = $this->encryptPassword( $str );
+ $this->mNewpassword = self::crypt( $str );
if ( $throttle ) {
$this->mNewpassTime = wfTimestampNow();
}
@@ -1515,20 +1599,23 @@ class User {
$expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgPasswordReminderResendTime * 3600;
return time() < $expiry;
}
-
+
function getEmail() {
$this->load();
+ wfRunHooks( 'UserGetEmail', array( $this, &$this->mEmail ) );
return $this->mEmail;
}
function getEmailAuthenticationTimestamp() {
$this->load();
+ wfRunHooks( 'UserGetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
return $this->mEmailAuthenticated;
}
function setEmail( $str ) {
$this->load();
$this->mEmail = $str;
+ wfRunHooks( 'UserSetEmail', array( $this, &$this->mEmail ) );
}
function getRealName() {
@@ -1542,8 +1629,8 @@ class User {
}
/**
- * @param string $oname The option to check
- * @param string $defaultOverride A default value returned if the option does not exist
+ * @param $oname String: the option to check
+ * @param $defaultOverride String: A default value returned if the option does not exist
* @return string
*/
function getOption( $oname, $defaultOverride = '' ) {
@@ -1564,7 +1651,7 @@ class User {
}
/**
- * Get the user's date preference, including some important migration for
+ * Get the user's date preference, including some important migration for
* old user rows.
*/
function getDatePreference() {
@@ -1581,17 +1668,17 @@ class User {
}
/**
- * @param string $oname The option to check
+ * @param $oname String: the option to check
* @return bool False if the option is not selected, true if it is
*/
function getBoolOption( $oname ) {
return (bool)$this->getOption( $oname );
}
-
+
/**
* Get an option as an integer value from the source string.
- * @param string $oname The option to check
- * @param int $default Optional value to return if option is unset/blank.
+ * @param $oname String: the option to check
+ * @param $default Int: optional value to return if option is unset/blank.
* @return int
*/
function getIntOption( $oname, $default=0 ) {
@@ -1613,9 +1700,16 @@ class User {
}
// Filter out any newlines that may have passed through input validation.
// Newlines are used to separate items in the options blob.
- $val = str_replace( "\r\n", "\n", $val );
- $val = str_replace( "\r", "\n", $val );
- $val = str_replace( "\n", " ", $val );
+ if( $val ) {
+ $val = str_replace( "\r\n", "\n", $val );
+ $val = str_replace( "\r", "\n", $val );
+ $val = str_replace( "\n", " ", $val );
+ }
+ // Explicitly NULL values should refer to defaults
+ global $wgDefaultUserOptions;
+ if( is_null($val) && isset($wgDefaultUserOptions[$oname]) ) {
+ $val = $wgDefaultUserOptions[$oname];
+ }
$this->mOptions[$oname] = $val;
}
@@ -1623,6 +1717,8 @@ class User {
if ( is_null( $this->mRights ) ) {
$this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
wfRunHooks( 'UserGetRights', array( $this, &$this->mRights ) );
+ // Force reindexation of rights when a hook has unset one of them
+ $this->mRights = array_values( $this->mRights );
}
return $this->mRights;
}
@@ -1641,15 +1737,14 @@ 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 boolean $recache Don't use the cache
+ * @param $recache Boolean: don't use the cache
* @return array of strings
*/
function getEffectiveGroups( $recache = false ) {
if ( $recache || is_null( $this->mEffectiveGroups ) ) {
- $this->load();
- $this->mEffectiveGroups = $this->mGroups;
+ $this->mEffectiveGroups = $this->getGroups();
$this->mEffectiveGroups[] = '*';
- if( $this->mId ) {
+ if( $this->getId() ) {
$this->mEffectiveGroups[] = 'user';
$this->mEffectiveGroups = array_unique( array_merge(
@@ -1663,28 +1758,27 @@ class User {
}
return $this->mEffectiveGroups;
}
-
+
/* Return the edit count for the user. This is where User::edits should have been */
function getEditCount() {
if ($this->mId) {
if ( !isset( $this->mEditCount ) ) {
/* Populate the count, if it has not been populated yet */
$this->mEditCount = User::edits($this->mId);
- }
+ }
return $this->mEditCount;
} else {
/* nil */
return null;
}
}
-
+
/**
* Add the user to the given group.
* This takes immediate effect.
- * @param string $group
+ * @param $group string
*/
function addGroup( $group ) {
- $this->load();
$dbw = wfGetDB( DB_MASTER );
if( $this->getId() ) {
$dbw->insert( 'user_groups',
@@ -1696,6 +1790,7 @@ class User {
array( 'IGNORE' ) );
}
+ $this->loadGroups();
$this->mGroups[] = $group;
$this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
@@ -1705,7 +1800,7 @@ class User {
/**
* Remove the user from the given group.
* This takes immediate effect.
- * @param string $group
+ * @param $group string
*/
function removeGroup( $group ) {
$this->load();
@@ -1717,6 +1812,7 @@ class User {
),
'User::removeGroup' );
+ $this->loadGroups();
$this->mGroups = array_diff( $this->mGroups, array( $group ) );
$this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
@@ -1749,12 +1845,13 @@ class User {
* @deprecated
*/
function isBot() {
+ wfDeprecated( __METHOD__ );
return $this->isAllowed( 'bot' );
}
/**
* Check if user is allowed to access a feature / make an action
- * @param string $action Action to be checked
+ * @param $action String: action to be checked
* @return boolean True: action is allowed, False: action should not be allowed
*/
function isAllowed($action='') {
@@ -1766,6 +1863,24 @@ class User {
}
/**
+ * Check whether to enable recent changes patrol features for this user
+ * @return bool
+ */
+ public function useRCPatrol() {
+ global $wgUseRCPatrol;
+ return( $wgUseRCPatrol && ($this->isAllowed('patrol') || $this->isAllowed('patrolmarks')) );
+ }
+
+ /**
+ * Check whether to enable recent changes patrol features for this user
+ * @return bool
+ */
+ public function useNPPatrol() {
+ global $wgUseRCPatrol, $wgUseNPPatrol;
+ return( ($wgUseRCPatrol || $wgUseNPPatrol) && ($this->isAllowed('patrol') || $this->isAllowed('patrolmarks')) );
+ }
+
+ /**
* Load a skin if it doesn't exist or return it
* @todo FIXME : need to check the old failback system [AV]
*/
@@ -1785,7 +1900,7 @@ class User {
}
/**#@+
- * @param string $title Article title to look at
+ * @param $title Title: article title to look at
*/
/**
@@ -1821,7 +1936,7 @@ class User {
* the next change of the page if it's watched etc.
*/
function clearNotification( &$title ) {
- global $wgUser, $wgUseEnotif;
+ global $wgUser, $wgUseEnotif, $wgShowUpdatedMarker;
# Do nothing if the database is locked to writes
if( wfReadOnly() ) {
@@ -1835,7 +1950,7 @@ class User {
$this->setNewtalk( false );
}
- if( !$wgUseEnotif ) {
+ if( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
return;
}
@@ -1852,7 +1967,7 @@ class User {
$title->getText() == $wgUser->getName())
{
$watched = true;
- } elseif ( $this->getID() == $wgUser->getID() ) {
+ } elseif ( $this->getId() == $wgUser->getId() ) {
$watched = $title->userIsWatching();
} else {
$watched = true;
@@ -1869,7 +1984,7 @@ class User {
'wl_title' => $title->getDBkey(),
'wl_namespace' => $title->getNamespace(),
'wl_user' => $this->getID()
- ), 'User::clearLastVisited'
+ ), __METHOD__
);
}
}
@@ -1881,17 +1996,15 @@ class User {
* If e-notif e-mails are on, they will receive notification mails on
* the next change of any watched page.
*
- * @param int $currentUser user ID number
- * @public
+ * @param $currentUser Int: user ID number
*/
function clearAllNotifications( $currentUser ) {
- global $wgUseEnotif;
- if ( !$wgUseEnotif ) {
+ global $wgUseEnotif, $wgShowUpdatedMarker;
+ if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
$this->setNewtalk( false );
return;
}
if( $currentUser != 0 ) {
-
$dbw = wfGetDB( DB_MASTER );
$dbw->update( 'watchlist',
array( /* SET */
@@ -1900,8 +2013,7 @@ class User {
'wl_user' => $currentUser
), __METHOD__
);
-
- # we also need to clear here the "you have new message" notification for the own user_talk page
+ # We also need to clear here the "you have new message" notification for the own user_talk page
# This is cleared one page view later in Article::viewUpdates();
}
}
@@ -1936,24 +2048,73 @@ class User {
}
}
}
+
+ 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 );
+ }
+ }
+
+ protected function clearCookie( $name ) {
+ $this->setCookie( $name, '', time() - 86400 );
+ }
function setCookies() {
- global $wgCookieExpiration, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix;
$this->load();
if ( 0 == $this->mId ) return;
- $exp = time() + $wgCookieExpiration;
-
- $_SESSION['wsUserID'] = $this->mId;
- setcookie( $wgCookiePrefix.'UserID', $this->mId, $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
-
- $_SESSION['wsUserName'] = $this->getName();
- setcookie( $wgCookiePrefix.'UserName', $this->getName(), $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
-
- $_SESSION['wsToken'] = $this->mToken;
+ $session = array(
+ 'wsUserID' => $this->mId,
+ 'wsToken' => $this->mToken,
+ 'wsUserName' => $this->getName()
+ );
+ $cookies = array(
+ 'UserID' => $this->mId,
+ 'UserName' => $this->getName(),
+ );
if ( 1 == $this->getOption( 'rememberpassword' ) ) {
- setcookie( $wgCookiePrefix.'Token', $this->mToken, $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
+ $cookies['Token'] = $this->mToken;
} else {
- setcookie( $wgCookiePrefix.'Token', '', time() - 3600 );
+ $cookies['Token'] = false;
+ }
+
+ wfRunHooks( 'UserSetCookies', array( $this, &$session, &$cookies ) );
+ $_SESSION = $session + $_SESSION;
+ foreach ( $cookies as $name => $value ) {
+ if ( $value === false ) {
+ $this->clearCookie( $name );
+ } else {
+ $this->setCookie( $name, $value );
+ }
}
}
@@ -1964,7 +2125,6 @@ class User {
global $wgUser;
if( wfRunHooks( 'UserLogout', array(&$this) ) ) {
$this->doLogout();
- wfRunHooks( 'UserLogoutComplete', array(&$wgUser) );
}
}
@@ -1973,16 +2133,15 @@ class User {
* Clears the cookies and session, resets the instance cache
*/
function doLogout() {
- global $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix;
$this->clearInstanceCache( 'defaults' );
$_SESSION['wsUserID'] = 0;
- setcookie( $wgCookiePrefix.'UserID', '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
- setcookie( $wgCookiePrefix.'Token', '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
+ $this->clearCookie( 'UserID' );
+ $this->clearCookie( 'Token' );
# Remember when user logged out, to prevent seeing cached pages
- setcookie( $wgCookiePrefix.'LoggedOut', wfTimestampNow(), time() + 86400, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
+ $this->setCookie( 'LoggedOut', wfTimestampNow(), time() + 86400 );
}
/**
@@ -1993,7 +2152,7 @@ class User {
$this->load();
if ( wfReadOnly() ) { return; }
if ( 0 == $this->mId ) { return; }
-
+
$this->mTouched = self::newTouchedTimestamp();
$dbw = wfGetDB( DB_MASTER );
@@ -2008,15 +2167,17 @@ class User {
'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
'user_options' => $this->encodeOptions(),
'user_touched' => $dbw->timestamp($this->mTouched),
- 'user_token' => $this->mToken
+ 'user_token' => $this->mToken,
+ 'user_email_token' => $this->mEmailToken,
+ 'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
), array( /* WHERE */
'user_id' => $this->mId
), __METHOD__
);
+ wfRunHooks( 'UserSaveSettings', array( $this ) );
$this->clearSharedCache();
}
-
/**
* Checks if a user with the given name exists, returns the ID.
*/
@@ -2035,8 +2196,8 @@ class User {
/**
* Add a user to the database, return the user object
*
- * @param string $name The user's name
- * @param array $params Associative array of non-default parameters to save to the database:
+ * @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
@@ -2082,7 +2243,7 @@ class User {
}
return $newUser;
}
-
+
/**
* Add an existing user object to the database
*/
@@ -2184,7 +2345,6 @@ class User {
/**
* Determine if the user is blocked from using Special:Emailuser.
*
- * @public
* @return boolean
*/
function isBlockedFromEmailuser() {
@@ -2199,13 +2359,14 @@ class User {
/**
* @deprecated
*/
- function setLoaded( $loaded ) {}
+ function setLoaded( $loaded ) {
+ wfDeprecated( __METHOD__ );
+ }
/**
* Get this user's personal page title.
*
* @return Title
- * @public
*/
function getUserPage() {
return Title::makeTitle( NS_USER, $this->getName() );
@@ -2215,7 +2376,6 @@ class User {
* Get this user's talk page title.
*
* @return Title
- * @public
*/
function getTalkPage() {
$title = $this->getUserPage();
@@ -2244,10 +2404,36 @@ class User {
function isNewbie() {
return !$this->isAllowed( 'autoconfirmed' );
}
+
+ /**
+ * 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
+ */
+ public function isActiveEditor() {
+ global $wgActiveUserEditCount, $wgActiveUserDays;
+ $dbr = wfGetDB( DB_SLAVE );
+
+ // Stolen without shame from RC
+ $cutoff_unixtime = time() - ( $wgActiveUserDays * 86400 );
+ $cutoff_unixtime = $cutoff_unixtime - ( $cutoff_unixtime % 86400 );
+ $oldTime = $dbr->addQuotes( $dbr->timestamp( $cutoff_unixtime ) );
+
+ $res = $dbr->select( 'revision', '1',
+ array( 'rev_user_text' => $this->getName(), "rev_timestamp > $oldTime"),
+ __METHOD__,
+ array('LIMIT' => $wgActiveUserEditCount ) );
+
+ $count = $dbr->numRows($res);
+ $dbr->freeResult($res);
+
+ return $count == $wgActiveUserEditCount;
+ }
/**
* Check to see if the given clear-text password is one of the accepted passwords
- * @param string $password User password.
+ * @param $password String: user password.
* @return bool True if the given password is correct otherwise False.
*/
function checkPassword( $password ) {
@@ -2272,28 +2458,26 @@ class User {
/* Auth plugin doesn't allow local authentication for this user name */
return false;
}
- $ep = $this->encryptPassword( $password );
- if ( 0 == strcmp( $ep, $this->mPassword ) ) {
+ if ( self::comparePasswords( $this->mPassword, $password, $this->mId ) ) {
return true;
} elseif ( function_exists( 'iconv' ) ) {
# Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
# Check for this with iconv
- $cp1252hash = $this->encryptPassword( iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password ) );
- if ( 0 == strcmp( $cp1252hash, $this->mPassword ) ) {
+ $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password );
+ if ( self::comparePasswords( $this->mPassword, $cp1252Password, $this->mId ) ) {
return true;
}
}
return false;
}
-
+
/**
* Check if the given clear-text password matches the temporary password
* sent by e-mail for password reset operations.
* @return bool
*/
function checkTemporaryPassword( $plaintext ) {
- $hash = $this->encryptPassword( $plaintext );
- return $hash === $this->mNewpassword;
+ return self::comparePasswords( $this->mNewpassword, $plaintext, $this->getId() );
}
/**
@@ -2302,10 +2486,9 @@ class User {
* login credentials aren't being hijacked with a foreign form
* submission.
*
- * @param mixed $salt - Optional function-specific data for hash.
- * Use a string or an array of strings.
+ * @param $salt Mixed: optional function-specific data for hash.
+ * Use a string or an array of strings.
* @return string
- * @public
*/
function editToken( $salt = '' ) {
if ( $this->isAnon() ) {
@@ -2340,10 +2523,9 @@ class User {
* user's own login session, not a form submission from a third-party
* site.
*
- * @param string $val - the input value to compare
- * @param string $salt - Optional function-specific data for hash
+ * @param $val String: the input value to compare
+ * @param $salt String: optional function-specific data for hash
* @return bool
- * @public
*/
function matchEditToken( $val, $salt = '' ) {
$sessionToken = $this->editToken( $salt );
@@ -2362,30 +2544,39 @@ class User {
}
/**
- * Generate a new e-mail confirmation token and send a confirmation
+ * 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.
*/
function sendConfirmationMail() {
- global $wgContLang;
+ global $wgLang;
$expiration = null; // gets passed-by-ref and defined in next line.
- $url = $this->confirmationTokenUrl( $expiration );
+ $token = $this->confirmationToken( $expiration );
+ $url = $this->confirmationTokenUrl( $token );
+ $invalidateURL = $this->invalidationTokenUrl( $token );
+ $this->saveSettings();
+
return $this->sendMail( wfMsg( 'confirmemail_subject' ),
wfMsg( 'confirmemail_body',
wfGetIP(),
$this->getName(),
$url,
- $wgContLang->timeanddate( $expiration, false ) ) );
+ $wgLang->timeanddate( $expiration, false ),
+ $invalidateURL ) );
}
/**
* Send an e-mail to this user's account. Does not check for
* confirmed status or validity.
*
- * @param string $subject
- * @param string $body
- * @param string $from Optional from address; default $wgPasswordSender will be used otherwise.
+ * @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.
*/
function sendMail( $subject, $body, $from = null, $replyto = null ) {
@@ -2402,6 +2593,10 @@ class User {
/**
* Generate, store, and return a new e-mail confirmation code.
* A hash (unsalted since it's used as a key) is stored.
+ *
+ * Call saveSettings() after calling this function to commit
+ * this change to the database.
+ *
* @param &$expiration mixed output: accepts the expiration time
* @return string
* @private
@@ -2410,43 +2605,82 @@ class User {
$now = time();
$expires = $now + 7 * 24 * 60 * 60;
$expiration = wfTimestamp( TS_MW, $expires );
-
$token = $this->generateToken( $this->mId . $this->mEmail . $expires );
$hash = md5( $token );
-
- $dbw = wfGetDB( DB_MASTER );
- $dbw->update( 'user',
- array( 'user_email_token' => $hash,
- 'user_email_token_expires' => $dbw->timestamp( $expires ) ),
- array( 'user_id' => $this->mId ),
- __METHOD__ );
-
+ $this->load();
+ $this->mEmailToken = $hash;
+ $this->mEmailTokenExpires = $expiration;
return $token;
}
/**
- * Generate and store a new e-mail confirmation token, and return
- * the URL the user can use to confirm.
- * @param &$expiration mixed output: accepts the expiration time
+ * Return a URL the user can use to confirm their email address.
+ * @param $token accepts the email confirmation token
* @return string
* @private
*/
- function confirmationTokenUrl( &$expiration ) {
- $token = $this->confirmationToken( $expiration );
- $title = SpecialPage::getTitleFor( 'Confirmemail', $token );
- return $title->getFullUrl();
+ function confirmationTokenUrl( $token ) {
+ return $this->getTokenUrl( 'ConfirmEmail', $token );
+ }
+ /**
+ * Return a URL the user can use to invalidate their email address.
+ * @param $token accepts the email confirmation token
+ * @return string
+ * @private
+ */
+ function invalidationTokenUrl( $token ) {
+ return $this->getTokenUrl( 'Invalidateemail', $token );
+ }
+
+ /**
+ * Internal function to format the e-mail validation/invalidation URLs.
+ * 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
+ * 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).
+ */
+ protected function getTokenUrl( $page, $token ) {
+ global $wgArticlePath;
+ return wfExpandUrl(
+ str_replace(
+ '$1',
+ "Special:$page/$token",
+ $wgArticlePath ) );
}
/**
- * Mark the e-mail address confirmed and save.
+ * Mark the e-mail address confirmed.
+ *
+ * Call saveSettings() after calling this function to commit the change.
*/
function confirmEmail() {
+ $this->setEmailAuthenticationTimestamp( wfTimestampNow() );
+ return true;
+ }
+
+ /**
+ * Invalidate the user's email confirmation, unauthenticate the email
+ * if it was already confirmed.
+ *
+ * Call saveSettings() after calling this function to commit the change.
+ */
+ function invalidateEmail() {
$this->load();
- $this->mEmailAuthenticated = wfTimestampNow();
- $this->saveSettings();
+ $this->mEmailToken = null;
+ $this->mEmailTokenExpires = null;
+ $this->setEmailAuthenticationTimestamp( null );
return true;
}
+ function setEmailAuthenticationTimestamp( $timestamp ) {
+ $this->load();
+ $this->mEmailAuthenticated = $timestamp;
+ wfRunHooks( 'UserSetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
+ }
+
/**
* Is this user allowed to send e-mails within limits of current
* site configuration?
@@ -2493,7 +2727,7 @@ class User {
return $confirmed;
}
}
-
+
/**
* Return true if there is an outstanding request for e-mail confirmation.
* @return bool
@@ -2505,7 +2739,7 @@ class User {
$this->mEmailToken &&
$this->mEmailTokenExpires > wfTimestamp();
}
-
+
/**
* Get the timestamp of account creation, or false for
* non-existent/anonymous user accounts
@@ -2519,9 +2753,8 @@ class User {
}
/**
- * @param array $groups list of groups
+ * @param $groups Array: list of groups
* @return array list of permission key names for given groups combined
- * @static
*/
static function getGroupPermissions( $groups ) {
global $wgGroupPermissions;
@@ -2536,9 +2769,8 @@ class User {
}
/**
- * @param string $group key name
+ * @param $group String: key name
* @return string localized descriptive name for group, if provided
- * @static
*/
static function getGroupName( $group ) {
global $wgMessageCache;
@@ -2551,9 +2783,8 @@ class User {
}
/**
- * @param string $group key name
+ * @param $group String: key name
* @return string localized descriptive name for member of a group, if provided
- * @static
*/
static function getGroupMember( $group ) {
global $wgMessageCache;
@@ -2567,11 +2798,10 @@ class User {
/**
* Return the set of defined explicit groups.
- * The *, 'user', 'autoconfirmed' and 'emailconfirmed'
- * groups are not included, as they are defined
- * automatically, not in the database.
+ * The implicit groups (by default *, 'user' and 'autoconfirmed')
+ * are not included, as they are defined automatically,
+ * not in the database.
* @return array
- * @static
*/
static function getAllGroups() {
global $wgGroupPermissions;
@@ -2582,6 +2812,22 @@ class User {
}
/**
+ * Get a list of all available permissions
+ */
+ static function getAllRights() {
+ if ( self::$mAllRights === false ) {
+ global $wgAvailableRights;
+ if ( count( $wgAvailableRights ) ) {
+ self::$mAllRights = array_unique( array_merge( self::$mCoreRights, $wgAvailableRights ) );
+ } else {
+ self::$mAllRights = self::$mCoreRights;
+ }
+ wfRunHooks( 'UserGetAllRights', array( &self::$mAllRights ) );
+ }
+ return self::$mAllRights;
+ }
+
+ /**
* Get a list of implicit groups
*
* @return array
@@ -2651,7 +2897,7 @@ class User {
return $text;
}
}
-
+
/**
* Increment the user's edit-count field.
* Will have no effect for anonymous users.
@@ -2663,7 +2909,7 @@ class User {
array( 'user_editcount=user_editcount+1' ),
array( 'user_id' => $this->getId() ),
__METHOD__ );
-
+
// Lazy initialization check...
if( $dbw->affectedRows() == 0 ) {
// Pull from a slave to be less cruel to servers
@@ -2673,7 +2919,7 @@ class User {
'COUNT(rev_user)',
array( 'rev_user' => $this->getId() ),
__METHOD__ );
-
+
// Now here's a goddamn hack...
if( $dbr !== $dbw ) {
// If we actually have a slave server, the count is
@@ -2685,7 +2931,7 @@ class User {
// count we just read includes the revision that was
// just added in the working transaction.
}
-
+
$dbw->update( 'user',
array( 'user_editcount' => $count ),
array( 'user_id' => $this->getId() ),
@@ -2695,7 +2941,72 @@ class User {
// edit count in user cache too
$this->invalidateCache();
}
-}
+
+ static function getRightDescription( $right ) {
+ global $wgMessageCache;
+ $wgMessageCache->loadAllMessages();
+ $key = "right-$right";
+ $name = wfMsg( $key );
+ return $name == '' || wfEmptyMsg( $key, $name )
+ ? $right
+ : $name;
+ }
+ /**
+ * Make an old-style password hash
+ *
+ * @param $password String: plain-text password
+ * @param $userId String: user ID
+ */
+ static function oldCrypt( $password, $userId ) {
+ global $wgPasswordSalt;
+ if ( $wgPasswordSalt ) {
+ return md5( $userId . '-' . md5( $password ) );
+ } else {
+ return md5( $password );
+ }
+ }
+ /**
+ * 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.
+ */
+ static function crypt( $password, $salt = false ) {
+ global $wgPasswordSalt;
+ if($wgPasswordSalt) {
+ if ( $salt === false ) {
+ $salt = substr( wfGenerateToken(), 0, 8 );
+ }
+ return ':B:' . $salt . ':' . md5( $salt . '-' . md5( $password ) );
+ } else {
+ return ':A:' . md5( $password);
+ }
+ }
+
+ /**
+ * 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
+ */
+ static function comparePasswords( $hash, $password, $userId = false ) {
+ $m = false;
+ $type = substr( $hash, 0, 3 );
+ if ( $type == ':A:' ) {
+ # Unsalted
+ return md5( $password ) === substr( $hash, 3 );
+ } elseif ( $type == ':B:' ) {
+ # Salted
+ list( $salt, $realHash ) = explode( ':', substr( $hash, 3 ), 2 );
+ return md5( $salt.'-'.md5( $password ) ) == $realHash;
+ } else {
+ # Old-style
+ return self::oldCrypt( $password, $userId ) === $hash;
+ }
+ }
+}
diff --git a/includes/UserArray.php b/includes/UserArray.php
new file mode 100644
index 00000000..27847e6f
--- /dev/null
+++ b/includes/UserArray.php
@@ -0,0 +1,62 @@
+<?php
+
+abstract class UserArray implements Iterator {
+ static function newFromResult( $res ) {
+ $userArray = null;
+ if ( !wfRunHooks( 'UserArrayFromResult', array( &$userArray, $res ) ) ) {
+ return null;
+ }
+ if ( $userArray === null ) {
+ $userArray = self::newFromResult_internal( $res );
+ }
+ return $userArray;
+ }
+
+ protected static function newFromResult_internal( $res ) {
+ $userArray = new UserArrayFromResult( $res );
+ return $userArray;
+ }
+}
+
+class UserArrayFromResult extends UserArray {
+ 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 = User::newFromRow( $row );
+ }
+ }
+
+ 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/UserMailer.php b/includes/UserMailer.php
index d043a6b5..0bc4268f 100644
--- a/includes/UserMailer.php
+++ b/includes/UserMailer.php
@@ -29,8 +29,8 @@
*/
class MailAddress {
/**
- * @param mixed $address String with an email address, or a User object
- * @param string $name Human-readable name if a string address is given
+ * @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 ) {
if( is_object( $address ) && $address instanceof User ) {
@@ -126,10 +126,12 @@ class UserMailer {
$headers['From'] = $from->toString();
- if ($wgEnotifImpersonal)
+ if ($wgEnotifImpersonal) {
$headers['To'] = 'undisclosed-recipients:;';
- else
- $headers['To'] = $to->toString();
+ }
+ else {
+ $headers['To'] = implode( ", ", (array )$dest );
+ }
if ( $replyto ) {
$headers['Reply-To'] = $replyto->toString();
@@ -160,7 +162,7 @@ class UserMailer {
# In the following $headers = expression we removed "Reply-To: {$from}\r\n" , because it is treated differently
# (fifth parameter of the PHP mail function, see some lines below)
- # Line endings need to be different on Unix and Windows due to
+ # Line endings need to be different on Unix and Windows due to
# the bug described at http://trac.wordpress.org/ticket/2603
if ( wfIsWindows() ) {
$body = str_replace( "\n", "\r\n", $body );
@@ -257,13 +259,13 @@ class EmailNotification {
* @private
*/
var $to, $subject, $body, $replyto, $from;
- var $user, $title, $timestamp, $summary, $minorEdit, $oldid;
+ var $user, $title, $timestamp, $summary, $minorEdit, $oldid, $composed_common, $editor;
var $mailTargets = array();
/**@}}*/
/**
- * Send emails corresponding to the user $editor editing the page $title.
+ * Send emails corresponding to the user $editor editing the page $title.
* Also updates wl_notificationtimestamp.
*
* May be deferred via the job queue.
@@ -277,13 +279,14 @@ class EmailNotification {
*/
function notifyOnPageChange($editor, $title, $timestamp, $summary, $minorEdit, $oldid = false) {
global $wgEnotifUseJobQ;
-
+
if( $title->getNamespace() < 0 )
return;
if ($wgEnotifUseJobQ) {
$params = array(
"editor" => $editor->getName(),
+ "editorID" => $editor->getID(),
"timestamp" => $timestamp,
"summary" => $summary,
"minorEdit" => $minorEdit,
@@ -297,9 +300,9 @@ class EmailNotification {
}
/*
- * Immediate version of notifyOnPageChange().
+ * Immediate version of notifyOnPageChange().
*
- * Send emails corresponding to the user $editor editing the page $title.
+ * Send emails corresponding to the user $editor editing the page $title.
* Also updates wl_notificationtimestamp.
*
* @param $editor User object
@@ -311,7 +314,7 @@ class EmailNotification {
*/
function actuallyNotifyOnPageChange($editor, $title, $timestamp, $summary, $minorEdit, $oldid=false) {
- # we use $wgEmergencyContact as sender's address
+ # we use $wgPasswordSender as sender's address
global $wgEnotifWatchlist;
global $wgEnotifMinorEdits, $wgEnotifUserTalk, $wgShowUpdatedMarker;
global $wgEnotifImpersonal;
@@ -331,7 +334,8 @@ class EmailNotification {
$this->summary = $summary;
$this->minorEdit = $minorEdit;
$this->oldid = $oldid;
- $this->composeCommonMailtext($editor);
+ $this->editor = $editor;
+ $this->composed_common = false;
$userTalkId = false;
@@ -351,29 +355,32 @@ class EmailNotification {
}
}
-
if ( $wgEnotifWatchlist ) {
// Send updates to watchers other than the current editor
- $userCondition = 'wl_user <> ' . intval( $editor->getId() );
+ $userCondition = 'wl_user != ' . $editor->getID();
if ( $userTalkId !== false ) {
- // Already sent an email to this person
- $userCondition .= ' AND wl_user <> ' . intval( $userTalkId );
+ // Already sent an email to this person
+ $userCondition .= ' AND wl_user != ' . intval( $userTalkId );
}
$dbr = wfGetDB( DB_SLAVE );
- $res = $dbr->select( 'watchlist', array( 'wl_user' ),
+ list( $user ) = $dbr->tableNamesN( 'user' );
+
+ $res = $dbr->select( array( 'watchlist', 'user' ),
+ array( "$user.*" ),
array(
+ 'wl_user=user_id',
'wl_title' => $title->getDBkey(),
'wl_namespace' => $title->getNamespace(),
$userCondition,
'wl_notificationtimestamp IS NULL',
), __METHOD__ );
+ $userArray = UserArray::newFromResult( $res );
- foreach ( $res as $row ) {
- $watchingUser = User::newFromId( $row->wl_user );
- if ( $watchingUser->getOption( 'enotifwatchlistpages' ) &&
- ( !$minorEdit || $watchingUser->getOption('enotifminoredits') ) &&
- $watchingUser->isEmailConfirmed() )
+ foreach ( $userArray as $watchingUser ) {
+ if ( $watchingUser->getOption( 'enotifwatchlistpages' ) &&
+ ( !$minorEdit || $watchingUser->getOption('enotifminoredits') ) &&
+ $watchingUser->isEmailConfirmed() )
{
$this->compose( $watchingUser );
}
@@ -381,8 +388,8 @@ class EmailNotification {
}
}
- global $wgUsersNotifedOnAllChanges;
- foreach ( $wgUsersNotifedOnAllChanges as $name ) {
+ global $wgUsersNotifiedOnAllChanges;
+ foreach ( $wgUsersNotifiedOnAllChanges as $name ) {
$user = User::newFromName( $name );
$this->compose( $user );
}
@@ -390,8 +397,9 @@ class EmailNotification {
$this->sendMails();
if ( $wgShowUpdatedMarker || $wgEnotifWatchlist ) {
- # mark the changed watch-listed page with a timestamp, so that the page is
- # listed with an "updated since your last visit" icon in the watch list, ...
+ # 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.
$dbw = wfGetDB( DB_MASTER );
$dbw->update( 'watchlist',
array( /* SET */
@@ -399,7 +407,8 @@ class EmailNotification {
), array( /* WHERE */
'wl_title' => $title->getDBkey(),
'wl_namespace' => $title->getNamespace(),
- 'wl_notificationtimestamp IS NULL'
+ 'wl_notificationtimestamp IS NULL',
+ 'wl_user != ' . $editor->getID()
), __METHOD__
);
}
@@ -410,11 +419,13 @@ class EmailNotification {
/**
* @private
*/
- function composeCommonMailtext($editor) {
- global $wgEmergencyContact, $wgNoReplyAddress;
+ function composeCommonMailtext() {
+ global $wgPasswordSender, $wgNoReplyAddress;
global $wgEnotifFromEditor, $wgEnotifRevealEditorAddress;
global $wgEnotifImpersonal;
+ $this->composed_common = true;
+
$summary = ($this->summary == '') ? ' - ' : $this->summary;
$medit = ($this->minorEdit) ? wfMsg( 'minoredit' ) : '';
@@ -445,7 +456,7 @@ class EmailNotification {
if ($wgEnotifImpersonal && $this->oldid)
/*
- * For impersonal mail, show a diff link to the last
+ * For impersonal mail, show a diff link to the last
* revision.
*/
$keys['$NEWPAGE'] = wfMsgForContent('enotif_lastdiff',
@@ -464,8 +475,9 @@ class EmailNotification {
# Reveal the page editor's address as REPLY-TO address only if
# the user has not opted-out and the option is enabled at the
# global configuration level.
+ $editor = $this->editor;
$name = $editor->getName();
- $adminAddress = new MailAddress( $wgEmergencyContact, 'WikiAdmin' );
+ $adminAddress = new MailAddress( $wgPasswordSender, 'WikiAdmin' );
$editorAddress = new MailAddress( $editor );
if( $wgEnotifRevealEditorAddress
&& ( $editor->getEmail() != '' )
@@ -513,6 +525,10 @@ class EmailNotification {
*/
function compose( $user ) {
global $wgEnotifImpersonal;
+
+ if ( !$this->composed_common )
+ $this->composeCommonMailtext();
+
if ( $wgEnotifImpersonal ) {
$this->mailTargets[] = new MailAddress( $user );
} else {
@@ -561,7 +577,7 @@ class EmailNotification {
}
/**
- * Same as sendPersonalised but does impersonal mail suitable for bulk
+ * Same as sendPersonalised but does impersonal mail suitable for bulk
* mailing. Takes an array of MailAddress objects.
*/
function sendImpersonal( $addresses ) {
@@ -576,7 +592,7 @@ class EmailNotification {
array( wfMsgForContent('enotif_impersonal_salutation'),
$wgLang->timeanddate($this->timestamp, true, false, false)),
$this->body);
-
+
return UserMailer::send($addresses, $this->from, $this->subject, $body, $this->replyto);
}
@@ -588,6 +604,7 @@ class EmailNotification {
function wfRFC822Phrase( $s ) {
return UserMailer::rfc822Phrase( $s );
}
+
function userMailer( $to, $from, $subject, $body, $replyto=null ) {
return UserMailer::send( $to, $from, $subject, $body, $replyto );
}
diff --git a/includes/UserRightsProxy.php b/includes/UserRightsProxy.php
index de0e770c..8a65a01a 100644
--- a/includes/UserRightsProxy.php
+++ b/includes/UserRightsProxy.php
@@ -12,7 +12,7 @@ class UserRightsProxy {
$this->name = $name;
$this->id = intval( $id );
}
-
+
/**
* Confirm the selected database name is a valid local interwiki database name.
* @return bool
@@ -21,7 +21,7 @@ class UserRightsProxy {
global $wgLocalDatabases;
return in_array( $database, $wgLocalDatabases );
}
-
+
public static function whoIs( $database, $id ) {
$user = self::newFromId( $database, $id );
if( $user ) {
@@ -30,7 +30,7 @@ class UserRightsProxy {
return false;
}
}
-
+
/**
* Factory function; get a remote user entry by ID number.
* @return UserRightsProxy or null if doesn't exist
@@ -42,7 +42,7 @@ class UserRightsProxy {
public static function newFromName( $database, $name ) {
return self::newFromLookup( $database, 'user_name', $name );
}
-
+
private static function newFromLookup( $database, $field, $value ) {
$db = self::getDB( $database );
if( $db ) {
@@ -62,51 +62,38 @@ class UserRightsProxy {
/**
* Open a database connection to work on for the requested user.
* This may be a new connection to another database for remote users.
- * @param string $database
+ * @param $database string
* @return Database or null if invalid selection
*/
- private static function getDB( $database ) {
+ public static function getDB( $database ) {
global $wgLocalDatabases, $wgDBname;
if( self::validDatabase( $database ) ) {
if( $database == $wgDBname ) {
// Hmm... this shouldn't happen though. :)
return wfGetDB( DB_MASTER );
} else {
- global $wgDBuser, $wgDBpassword;
- $server = self::getMaster( $database );
- return new Database( $server, $wgDBuser, $wgDBpassword, $database );
+ return wfGetDB( DB_MASTER, array(), $database );
}
}
return null;
}
-
- /**
- * Return the master server to connect to for the requested database.
- */
- private static function getMaster( $database ) {
- global $wgDBserver, $wgAlternateMaster;
- if( isset( $wgAlternateMaster[$database] ) ) {
- return $wgAlternateMaster[$database];
- }
- return $wgDBserver;
- }
-
+
public function getId() {
return $this->id;
}
-
+
public function isAnon() {
return $this->getId() == 0;
}
-
+
public function getName() {
return $this->name . '@' . $this->database;
}
-
+
public function getUserPage() {
return Title::makeTitle( NS_USER, $this->getName() );
}
-
+
// Replaces getUserGroups()
function getGroups() {
$res = $this->db->select( 'user_groups',
@@ -119,7 +106,7 @@ class UserRightsProxy {
}
return $groups;
}
-
+
// replaces addUserGroup
function addGroup( $group ) {
$this->db->insert( 'user_groups',
@@ -130,7 +117,7 @@ class UserRightsProxy {
__METHOD__,
array( 'IGNORE' ) );
}
-
+
// replaces removeUserGroup
function removeGroup( $group ) {
$this->db->delete( 'user_groups',
@@ -140,7 +127,7 @@ class UserRightsProxy {
),
__METHOD__ );
}
-
+
// replaces touchUser
function invalidateCache() {
$this->db->update( 'user',
@@ -157,5 +144,3 @@ class UserRightsProxy {
$wgMemc->delete( $key );
}
}
-
-?> \ No newline at end of file
diff --git a/includes/WatchedItem.php b/includes/WatchedItem.php
index e4de67c8..23fc6a74 100644
--- a/includes/WatchedItem.php
+++ b/includes/WatchedItem.php
@@ -1,10 +1,11 @@
<?php
/**
- *
+ * @file
+ * @ingroup Watchlist
*/
/**
- *
+ * @ingroup Watchlist
*/
class WatchedItem {
var $mTitle, $mUser;
@@ -114,8 +115,8 @@ class WatchedItem {
* Check if the given title already is watched by the user, and if so
* add watches on a new title. To be used for page renames and such.
*
- * @param Title $ot Page title to duplicate entries from, if present
- * @param Title $nt Page title to add watches on
+ * @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 ) {
WatchedItem::doDuplicateEntries( $ot->getSubjectPage(), $nt->getSubjectPage() );
@@ -156,8 +157,4 @@ class WatchedItem {
$dbw->replace( 'watchlist', array(array( 'wl_user', 'wl_namespace', 'wl_title')), $values, $fname );
return true;
}
-
-
}
-
-
diff --git a/includes/WatchlistEditor.php b/includes/WatchlistEditor.php
index 7e37dca7..fcfdb782 100644
--- a/includes/WatchlistEditor.php
+++ b/includes/WatchlistEditor.php
@@ -4,7 +4,7 @@
* Provides the UI through which users can perform editing
* operations on their watchlist
*
- * @addtogroup Watchlist
+ * @ingroup Watchlist
* @author Rob Church <robchur@gmail.com>
*/
class WatchlistEditor {
@@ -19,10 +19,10 @@ class WatchlistEditor {
/**
* Main execution point
*
- * @param User $user
- * @param OutputPage $output
- * @param WebRequest $request
- * @param int $mode
+ * @param $user User
+ * @param $output OutputPage
+ * @param $request WebRequest
+ * @param $mode int
*/
public function execute( $user, $output, $request, $mode ) {
global $wgUser;
@@ -77,23 +77,23 @@ class WatchlistEditor {
$this->showNormalForm( $output, $user );
}
}
-
+
/**
* Check the edit token from a form submission
*
- * @param WebRequest $request
- * @param User $user
+ * @param $request WebRequest
+ * @param $user User
* @return bool
*/
private function checkToken( $request, $user ) {
- return $user->matchEditToken( $request->getVal( 'token' ), 'watchlistedit' );
+ return $user->matchEditToken( $request->getVal( 'token' ), 'watchlistedit' );
}
-
+
/**
* Extract a list of titles from a blob of text, returning
* (prefixed) strings; unwatchable titles are ignored
*
- * @param mixed $list
+ * @param $list mixed
* @return array
*/
private function extractTitles( $list ) {
@@ -113,20 +113,20 @@ class WatchlistEditor {
}
return array_unique( $titles );
}
-
+
/**
* Print out a list of linked titles
*
* $titles can be an array of strings or Title objects; the former
* is preferred, since Titles are very memory-heavy
*
- * @param array $titles An array of strings, or Title objects
- * @param OutputPage $output
- * @param Skin $skin
+ * @param $titles An array of strings, or Title objects
+ * @param $output OutputPage
+ * @param $skin Skin
*/
private function showTitles( $titles, $output, $skin ) {
$talk = wfMsgHtml( 'talkpagelinktext' );
- // Do a batch existence check
+ // Do a batch existence check
$batch = new LinkBatch();
foreach( $titles as $title ) {
if( !$title instanceof Title )
@@ -149,11 +149,11 @@ class WatchlistEditor {
}
$output->addHtml( "</ul>\n" );
}
-
+
/**
* Count the number of titles on a user's watchlist, excluding talk pages
*
- * @param User $user
+ * @param $user User
* @return int
*/
private function countWatchlist( $user ) {
@@ -162,16 +162,16 @@ class WatchlistEditor {
$row = $dbr->fetchObject( $res );
return ceil( $row->count / 2 ); // Paranoia
}
-
+
/**
* Prepare a list of titles on a user's watchlist (excluding talk pages)
* and return an array of (prefixed) strings
*
- * @param User $user
+ * @param $user User
* @return array
*/
private function getWatchlist( $user ) {
- $list = array();
+ $list = array();
$dbr = wfGetDB( DB_MASTER );
$res = $dbr->select(
'watchlist',
@@ -187,17 +187,17 @@ class WatchlistEditor {
if( $title instanceof Title && !$title->isTalkPage() )
$list[] = $title->getPrefixedText();
}
- $res->free();
+ $res->free();
}
return $list;
}
-
+
/**
* Get a list of titles on a user's watchlist, excluding talk pages,
* and return as a two-dimensional array with namespace, title and
* redirect status
*
- * @param User $user
+ * @param $user User
* @return array
*/
private function getWatchlistInfo( $user ) {
@@ -205,7 +205,7 @@ class WatchlistEditor {
$dbr = wfGetDB( DB_MASTER );
$uid = intval( $user->getId() );
list( $watchlist, $page ) = $dbr->tableNamesN( 'watchlist', 'page' );
- $sql = "SELECT wl_namespace, wl_title, page_id, page_is_redirect
+ $sql = "SELECT wl_namespace, wl_title, page_id, page_len, page_is_redirect
FROM {$watchlist} LEFT JOIN {$page} ON ( wl_namespace = page_namespace
AND wl_title = page_title ) WHERE wl_user = {$uid}";
$res = $dbr->query( $sql, __METHOD__ );
@@ -216,7 +216,7 @@ class WatchlistEditor {
if( $title instanceof Title ) {
// Update the link cache while we're at it
if( $row->page_id ) {
- $cache->addGoodLinkObj( $row->page_id, $title );
+ $cache->addGoodLinkObj( $row->page_id, $title, $row->page_len, $row->page_is_redirect );
} else {
$cache->addBadLinkObj( $title );
}
@@ -228,13 +228,13 @@ class WatchlistEditor {
}
return $titles;
}
-
+
/**
* Show a message indicating the number of items on the user's watchlist,
* and return this count for additional checking
*
- * @param OutputPage $output
- * @param User $user
+ * @param $output OutputPage
+ * @param $user User
* @return int
*/
private function showItemCount( $output, $user ) {
@@ -246,11 +246,11 @@ class WatchlistEditor {
}
return $count;
}
-
+
/**
* Remove all titles from a user's watchlist
*
- * @param User $user
+ * @param $user User
*/
private function clearWatchlist( $user ) {
$dbw = wfGetDB( DB_MASTER );
@@ -263,8 +263,8 @@ class WatchlistEditor {
* $titles can be an array of strings or Title objects; the former
* is preferred, since Titles are very memory-heavy
*
- * @param array $titles An array of strings, or Title objects
- * @param User $user
+ * @param $titles An array of strings, or Title objects
+ * @param $user User
*/
private function watchTitles( $titles, $user ) {
$dbw = wfGetDB( DB_MASTER );
@@ -296,8 +296,8 @@ class WatchlistEditor {
* $titles can be an array of strings or Title objects; the former
* is preferred, since Titles are very memory-heavy
*
- * @param array $titles An array of strings, or Title objects
- * @param User $user
+ * @param $titles An array of strings, or Title objects
+ * @param $user User
*/
private function unwatchTitles( $titles, $user ) {
$dbw = wfGetDB( DB_MASTER );
@@ -326,12 +326,12 @@ class WatchlistEditor {
}
}
}
-
+
/**
* Show the standard watchlist editing form
*
- * @param OutputPage $output
- * @param User $user
+ * @param $output OutputPage
+ * @param $user User
*/
private function showNormalForm( $output, $user ) {
global $wgUser;
@@ -356,11 +356,11 @@ class WatchlistEditor {
$output->addHtml( $form );
}
}
-
+
/**
* Get the correct "heading" for a namespace
*
- * @param int $namespace
+ * @param $namespace int
* @return string
*/
private function getNamespaceHeading( $namespace ) {
@@ -368,14 +368,14 @@ class WatchlistEditor {
? wfMsgHtml( 'blanknamespace' )
: htmlspecialchars( $GLOBALS['wgContLang']->getFormattedNsText( $namespace ) );
}
-
+
/**
* Build a single list item containing a check box selecting a title
* and a link to that title, with various additional bits
*
- * @param Title $title
- * @param bool $redirect
- * @param Skin $skin
+ * @param $title Title
+ * @param $redirect bool
+ * @param $skin Skin
* @return string
*/
private function buildRemoveLine( $title, $redirect, $skin ) {
@@ -393,12 +393,12 @@ class WatchlistEditor {
. Xml::check( 'titles[]', false, array( 'value' => $title->getPrefixedText() ) )
. $link . ' (' . implode( ' | ', $tools ) . ')' . '</li>';
}
-
+
/**
* Show a form for editing the watchlist in "raw" mode
*
- * @param OutputPage $output
- * @param User $user
+ * @param $output OutputPage
+ * @param $user User
*/
public function showRawForm( $output, $user ) {
global $wgUser;
@@ -421,13 +421,13 @@ class WatchlistEditor {
$form .= '</fieldset></form>';
$output->addHtml( $form );
}
-
+
/**
* Determine whether we are editing the watchlist, and if so, what
* kind of editing operation
*
- * @param WebRequest $request
- * @param mixed $par
+ * @param $request WebRequest
+ * @param $par mixed
* @return int
*/
public static function getMode( $request, $par ) {
@@ -443,12 +443,12 @@ class WatchlistEditor {
return false;
}
}
-
+
/**
* Build a set of links for convenient navigation
* between watchlist viewing and editing modes
*
- * @param Skin $skin Skin to use
+ * @param $skin Skin to use
* @return string
*/
public static function buildTools( $skin ) {
@@ -459,5 +459,4 @@ class WatchlistEditor {
}
return implode( ' | ', $tools );
}
-
}
diff --git a/includes/WebRequest.php b/includes/WebRequest.php
index 944be3c9..3fce5845 100644
--- a/includes/WebRequest.php
+++ b/includes/WebRequest.php
@@ -23,7 +23,7 @@
/**
- * Some entry points may use this file without first enabling the
+ * Some entry points may use this file without first enabling the
* autoloader.
*/
if ( !function_exists( '__autoload' ) ) {
@@ -43,18 +43,20 @@ if ( !function_exists( '__autoload' ) ) {
*/
class WebRequest {
var $data = array();
-
+ var $headers;
+ private $_response;
+
function __construct() {
/// @fixme This preemptive de-quoting can interfere with other web libraries
/// and increases our memory footprint. It would be cleaner to do on
/// demand; but currently we have no wrapper for $_SERVER etc.
$this->checkMagicQuotes();
-
+
// POST overrides GET data
// We don't use $_REQUEST here to avoid interference from cookies...
$this->data = wfArrayMerge( $_GET, $_POST );
}
-
+
/**
* Check for title, action, and/or variant data in the URL
* and interpolate it into the GET variables.
@@ -77,8 +79,8 @@ class WebRequest {
}
$a = parse_url( $url );
if( $a ) {
- $path = $a['path'];
-
+ $path = isset( $a['path'] ) ? $a['path'] : '';
+
global $wgScript;
if( $path == $wgScript ) {
// Script inside a rewrite path?
@@ -87,17 +89,17 @@ class WebRequest {
}
// Raw PATH_INFO style
$matches = $this->extractTitle( $path, "$wgScript/$1" );
-
+
global $wgArticlePath;
if( !$matches && $wgArticlePath ) {
$matches = $this->extractTitle( $path, $wgArticlePath );
}
-
+
global $wgActionPaths;
if( !$matches && $wgActionPaths ) {
$matches = $this->extractTitle( $path, $wgActionPaths, 'action' );
}
-
+
global $wgVariantArticlePath, $wgContLang;
if( !$matches && $wgVariantArticlePath ) {
$variantPaths = array();
@@ -113,7 +115,7 @@ class WebRequest {
// http://bugs.php.net/bug.php?id=31892
// Also reported when ini_get('cgi.fix_pathinfo')==false
$matches['title'] = substr( $_SERVER['ORIG_PATH_INFO'], 1 );
-
+
} elseif ( isset( $_SERVER['PATH_INFO'] ) && ($_SERVER['PATH_INFO'] != '') ) {
// Regular old PATH_INFO yay
$matches['title'] = substr( $_SERVER['PATH_INFO'], 1 );
@@ -123,15 +125,15 @@ class WebRequest {
}
}
}
-
+
/**
* Internal URL rewriting function; tries to extract page title and,
* optionally, one other fixed parameter value from a URL path.
*
- * @param string $path the URL path given from the client
- * @param array $bases one or more URLs, optionally with $1 at the end
- * @param string $key if provided, the matching key in $bases will be
- * passed on as the value of this URL parameter
+ * @param $path string: the URL path given from the client
+ * @param $bases array: one or more URLs, optionally with $1 at the end
+ * @param $key string: if provided, the matching key in $bases will be
+ * passed on as the value of this URL parameter
* @return array of URL variables to interpolate; empty if no match
*/
private function extractTitle( $path, $bases, $key=false ) {
@@ -152,13 +154,11 @@ class WebRequest {
}
return array();
}
-
- private $_response;
/**
* Recursively strips slashes from the given array;
* used for undoing the evil that is magic_quotes_gpc.
- * @param array &$arr will be modified
+ * @param $arr array: will be modified
* @return array the original array
* @private
*/
@@ -181,7 +181,7 @@ class WebRequest {
* @private
*/
function checkMagicQuotes() {
- if ( get_magic_quotes_gpc() ) {
+ if ( function_exists( 'get_magic_quotes_gpc' ) && get_magic_quotes_gpc() ) {
$this->fix_magic_quotes( $_COOKIE );
$this->fix_magic_quotes( $_ENV );
$this->fix_magic_quotes( $_GET );
@@ -193,7 +193,7 @@ class WebRequest {
/**
* Recursively normalizes UTF-8 strings in the given array.
- * @param array $data string or array
+ * @param $data string or array
* @return cleaned-up version of the given
* @private
*/
@@ -211,9 +211,9 @@ class WebRequest {
/**
* Fetch a value from the given array or return $default if it's not set.
*
- * @param array $arr
- * @param string $name
- * @param mixed $default
+ * @param $arr array
+ * @param $name string
+ * @param $default mixed
* @return mixed
* @private
*/
@@ -236,12 +236,12 @@ class WebRequest {
/**
* Fetch a scalar from the input or return $default if it's not set.
- * Returns a string. Arrays are discarded. Useful for
- * non-freeform text inputs (e.g. predefined internal text keys
+ * Returns a string. Arrays are discarded. Useful for
+ * non-freeform text inputs (e.g. predefined internal text keys
* selected by a drop-down menu). For freeform input, see getText().
*
- * @param string $name
- * @param string $default optional default (or NULL)
+ * @param $name string
+ * @param $default string: optional default (or NULL)
* @return string
*/
function getVal( $name, $default = NULL ) {
@@ -261,8 +261,8 @@ class WebRequest {
* If source was scalar, will return an array with a single element.
* If no source and no default, returns NULL.
*
- * @param string $name
- * @param array $default optional default (or NULL)
+ * @param $name string
+ * @param $default array: optional default (or NULL)
* @return array
*/
function getArray( $name, $default = NULL ) {
@@ -273,15 +273,15 @@ class WebRequest {
return (array)$val;
}
}
-
+
/**
* Fetch an array of integers, or return $default if it's not set.
* If source was scalar, will return an array with a single element.
* If no source and no default, returns NULL.
* If an array is returned, contents are guaranteed to be integers.
*
- * @param string $name
- * @param array $default option default (or NULL)
+ * @param $name string
+ * @param $default array: option default (or NULL)
* @return array of ints
*/
function getIntArray( $name, $default = NULL ) {
@@ -296,8 +296,8 @@ class WebRequest {
* Fetch an integer value from the input or return $default if not set.
* Guaranteed to return an integer; non-numeric input will typically
* return 0.
- * @param string $name
- * @param int $default
+ * @param $name string
+ * @param $default int
* @return int
*/
function getInt( $name, $default = 0 ) {
@@ -308,7 +308,7 @@ class WebRequest {
* Fetch an integer value from the input or return null if empty.
* Guaranteed to return an integer or null; non-numeric input will
* typically return null.
- * @param string $name
+ * @param $name string
* @return int
*/
function getIntOrNull( $name ) {
@@ -322,8 +322,8 @@ class WebRequest {
* Fetch a boolean value from the input or return $default if not set.
* Guaranteed to return true or false, with normal PHP semantics for
* boolean interpretation of strings.
- * @param string $name
- * @param bool $default
+ * @param $name string
+ * @param $default bool
* @return bool
*/
function getBool( $name, $default = false ) {
@@ -334,7 +334,7 @@ class WebRequest {
* Return true if the named value is set in the input, whatever that
* value is (even "0"). Return false if the named value is not set.
* Example use is checking for the presence of check boxes in forms.
- * @param string $name
+ * @param $name string
* @return bool
*/
function getCheck( $name ) {
@@ -349,11 +349,11 @@ class WebRequest {
* set. \r is stripped from the text, and with some language modules there
* is an input transliteration applied. This should generally be used for
* form <textarea> and <input> fields. Used for user-supplied freeform text
- * input (for which input transformations may be required - e.g. Esperanto
+ * input (for which input transformations may be required - e.g. Esperanto
* x-coding).
*
- * @param string $name
- * @param string $default optional
+ * @param $name string
+ * @param $default string: optional
* @return string
*/
function getText( $name, $default = '' ) {
@@ -490,6 +490,26 @@ class WebRequest {
return htmlspecialchars( $this->appendQuery( $query ) );
}
+ function appendQueryValue( $key, $value, $onlyquery = false ) {
+ return $this->appendQueryArray( array( $key => $value ), $onlyquery );
+ }
+
+ /**
+ * Appends or replaces value of query variables.
+ * @param $array Array of values to replace/add to query
+ * @param $onlyquery Bool: whether to only return the query string and not
+ * the complete URL
+ * @return string
+ */
+ function appendQueryArray( $array, $onlyquery = false ) {
+ global $wgTitle;
+ $newquery = $_GET;
+ unset( $newquery['title'] );
+ $newquery = array_merge( $newquery, $array );
+ $query = wfArrayToCGI( $newquery );
+ return $onlyquery ? $query : $wgTitle->getLocalURL( $basequery );
+ }
+
/**
* Check for limit and offset parameters on the input, and return sensible
* defaults if not given. The limit must be positive and is capped at 5000.
@@ -560,7 +580,7 @@ class WebRequest {
*
* Other than this the name is not verified for being a safe filename.
*
- * @param $key String:
+ * @param $key String:
* @return string or NULL if no such file.
*/
function getFileName( $key ) {
@@ -576,19 +596,46 @@ class WebRequest {
wfDebug( "WebRequest::getFileName() '" . $_FILES[$key]['name'] . "' normalized to '$name'\n" );
return $name;
}
-
+
/**
- * Return a handle to WebResponse style object, for setting cookies,
+ * Return a handle to WebResponse style object, for setting cookies,
* headers and other stuff, for Request being worked on.
*/
function response() {
/* Lazy initialization of response object for this request */
if (!is_object($this->_response)) {
$this->_response = new WebResponse;
- }
+ }
return $this->_response;
}
-
+
+ /**
+ * Get a request header, or false if it isn't set
+ * @param $name String: case-insensitive header name
+ */
+ function getHeader( $name ) {
+ $name = strtoupper( $name );
+ if ( function_exists( 'apache_request_headers' ) ) {
+ if ( !isset( $this->headers ) ) {
+ $this->headers = array();
+ foreach ( apache_request_headers() as $tempName => $tempValue ) {
+ $this->headers[ strtoupper( $tempName ) ] = $tempValue;
+ }
+ }
+ if ( isset( $this->headers[$name] ) ) {
+ return $this->headers[$name];
+ } else {
+ return false;
+ }
+ } else {
+ $name = 'HTTP_' . str_replace( '-', '_', $name );
+ if ( isset( $_SERVER[$name] ) ) {
+ return $_SERVER[$name];
+ } else {
+ return false;
+ }
+ }
+ }
}
/**
@@ -599,9 +646,9 @@ class FauxRequest extends WebRequest {
var $wasPosted = false;
/**
- * @param array $data Array of *non*-urlencoded key => value pairs, the
+ * @param $data Array of *non*-urlencoded key => value pairs, the
* fake GET/POST values
- * @param bool $wasPosted Whether to treat the data as POST
+ * @param $wasPosted Bool: whether to treat the data as POST
*/
function FauxRequest( $data, $wasPosted = false ) {
if( is_array( $data ) ) {
@@ -610,6 +657,7 @@ class FauxRequest extends WebRequest {
throw new MWException( "FauxRequest() got bogus data" );
}
$this->wasPosted = $wasPosted;
+ $this->headers = array();
}
function getText( $name, $default = '' ) {
@@ -637,6 +685,8 @@ class FauxRequest extends WebRequest {
throw new MWException( 'FauxRequest::appendQuery() not implemented' );
}
-}
-
+ function getHeader( $name ) {
+ return isset( $this->headers[$name] ) ? $this->headers[$name] : false;
+ }
+}
diff --git a/includes/WebResponse.php b/includes/WebResponse.php
index f1578885..05023e15 100644
--- a/includes/WebResponse.php
+++ b/includes/WebResponse.php
@@ -16,5 +16,3 @@ class WebResponse {
setcookie($name,$value,$expire, $wgCookiePath, $wgCookieDomain, $wgCookieSecure);
}
}
-
-
diff --git a/includes/WebStart.php b/includes/WebStart.php
index a9a6ad5f..411c211c 100644
--- a/includes/WebStart.php
+++ b/includes/WebStart.php
@@ -1,7 +1,7 @@
<?php
-# This does the initial setup for a web request. It does some security checks,
-# starts the profiler and loads the configuration, and optionally loads
+# This does the initial setup for a web request. It does some security checks,
+# starts the profiler and loads the configuration, and optionally loads
# Setup.php depending on whether MW_NO_SETUP is defined.
# Test for PHP bug which breaks PHP 5.0.x on 64-bit...
@@ -65,40 +65,49 @@ unset( $IP );
# its purpose.
define( 'MEDIAWIKI', true );
+# Full path to working directory.
+# Makes it possible to for example to have effective exclude path in apc.
+# Also doesn't break installations using symlinked includes, like
+# dirname( __FILE__ ) would do.
+$IP = getenv( 'MW_INSTALL_PATH' );
+if ( $IP === false ) {
+ $IP = realpath( '.' );
+}
+
# Start profiler
-require_once( './StartProfiler.php' );
+require_once( "$IP/StartProfiler.php" );
wfProfileIn( 'WebStart.php-conf' );
# Load up some global defines.
-require_once( './includes/Defines.php' );
+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( './LocalSettings.php' ) ) {
- $IP = '.';
- require_once( './includes/DefaultSettings.php' ); # used for printing the version
- require_once( './includes/templates/NoLocalSettings.php' );
+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 this site setttings
-require_once( './LocalSettings.php' );
+# 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" );
wfProfileOut( 'WebStart.php-conf' );
-wfProfileIn( 'WebStart.php-ob_start' );
+wfProfileIn( 'WebStart.php-ob_start' );
# Initialise output buffering
if ( ob_get_level() ) {
# Someone's been mixing configuration data with code!
# How annoying.
} elseif ( !defined( 'MW_NO_OUTPUT_BUFFER' ) ) {
- require_once( './includes/OutputHandler.php' );
+ require_once( "$IP/includes/OutputHandler.php" );
ob_start( 'wfOutputHandler' );
}
-
wfProfileOut( 'WebStart.php-ob_start' );
if ( !defined( 'MW_NO_SETUP' ) ) {
- require_once( './includes/Setup.php' );
+ require_once( "$IP/includes/Setup.php" );
}
-
diff --git a/includes/Wiki.php b/includes/Wiki.php
index e0a57445..fa49290a 100644
--- a/includes/Wiki.php
+++ b/includes/Wiki.php
@@ -2,7 +2,6 @@
/**
* MediaWiki is the to-be base class for this whole project
*/
-
class MediaWiki {
var $GET; /* Stores the $_GET variables at time of creation, can be changed */
@@ -16,6 +15,9 @@ class MediaWiki {
/**
* Stores key/value pairs to circumvent global variables
* Note that keys are case-insensitive!
+ *
+ * @param $key String: key to store
+ * @param $value Mixed: value to put for the key
*/
function setVal( $key, &$value ) {
$key = strtolower( $key );
@@ -25,6 +27,9 @@ class MediaWiki {
/**
* Retrieves key/value pairs to circumvent global variables
* Note that keys are case-insensitive!
+ *
+ * @param $key String: key to get
+ * @param $default Mixed: default value if if the key doesn't exist
*/
function getVal( $key, $default = '' ) {
$key = strtolower( $key );
@@ -36,29 +41,41 @@ class MediaWiki {
/**
* Initialization of ... everything
- @return Article either the object to become $wgArticle, or NULL
+ * Performs the request too
+ *
+ * @param $title Title ($wgTitle)
+ * @param $article Article
+ * @param $output OutputPage
+ * @param $user User
+ * @param $request WebRequest
*/
- function initialize ( &$title, &$output, &$user, $request) {
- wfProfileIn( 'MediaWiki::initialize' );
- $this->preliminaryChecks ( $title, $output, $request ) ;
- $article = NULL;
+ function initialize( &$title, &$article, &$output, &$user, $request ) {
+ wfProfileIn( __METHOD__ );
+ $this->preliminaryChecks( $title, $output, $request ) ;
if ( !$this->initializeSpecialCases( $title, $output, $request ) ) {
- $article = $this->initializeArticle( $title, $request );
- if( is_object( $article ) ) {
+ $new_article = $this->initializeArticle( $title, $request );
+ if( is_object( $new_article ) ) {
+ $article = $new_article;
$this->performAction( $output, $article, $title, $user, $request );
- } elseif( is_string( $article ) ) {
- $output->redirect( $article );
+ } elseif( is_string( $new_article ) ) {
+ $output->redirect( $new_article );
} else {
throw new MWException( "Shouldn't happen: MediaWiki::initializeArticle() returned neither an object nor a URL" );
}
}
- wfProfileOut( 'MediaWiki::initialize' );
- return $article;
+ wfProfileOut( __METHOD__ );
}
+ /**
+ * Check if the maximum lag of database slaves is higher that $maxLag, and
+ * if it's the case, output an error message
+ *
+ * @param $maxLag int: maximum lag allowed for the request, as supplied by
+ * the client
+ * @return bool true if the request can continue
+ */
function checkMaxLag( $maxLag ) {
- global $wgLoadBalancer;
- list( $host, $lag ) = $wgLoadBalancer->getMaxLag();
+ list( $host, $lag ) = wfGetLB()->getMaxLag();
if ( $lag > $maxLag ) {
wfMaxlagError( $host, $lag, $maxLag );
return false;
@@ -71,30 +88,33 @@ class MediaWiki {
/**
* Checks some initial queries
* Note that $title here is *not* a Title object, but a string!
+ *
+ * @param $title String
+ * @param $action String
+ * @return Title object to be $wgTitle
*/
- function checkInitialQueries( $title,$action,&$output,$request, $lang) {
- if ($request->getVal( 'printable' ) == 'yes') {
- $output->setPrintable();
+ function checkInitialQueries( $title, $action ) {
+ global $wgOut, $wgRequest, $wgContLang;
+ if( $wgRequest->getVal( 'printable' ) == 'yes' ){
+ $wgOut->setPrintable();
}
- $ret = NULL ;
-
+ $ret = NULL;
if ( '' == $title && 'delete' != $action ) {
$ret = Title::newMainPage();
- } elseif ( $curid = $request->getInt( 'curid' ) ) {
+ } elseif ( $curid = $wgRequest->getInt( 'curid' ) ) {
# URLs like this are generated by RC, because rc_title isn't always accurate
$ret = Title::newFromID( $curid );
} 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($lang->getVariants()) > 1 && !is_null($ret) && $ret->getArticleID() == 0 )
- $lang->findVariantLink( $title, $ret );
+ // 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 = $request->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 );
@@ -102,19 +122,23 @@ class MediaWiki {
$ret = $rev->getTitle();
}
}
- return $ret ;
+ return $ret;
}
/**
* Checks for search query and anon-cannot-read case
+ *
+ * @param $title Title
+ * @param $output OutputPage
+ * @param $request WebRequest
*/
- function preliminaryChecks ( &$title, &$output, $request ) {
+ 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
// show the search page when using ugly URLs (bug 8054).
-
+
// Do this above the read whitelist check for security...
$title = SpecialPage::getTitleFor( 'Search' );
}
@@ -131,14 +155,22 @@ class MediaWiki {
}
/**
- * Initialize the object to be known as $wgArticle for special cases
+ * Initialize some special cases:
+ * - bad titles
+ * - local interwiki redirects
+ * - redirect loop
+ * - special pages
+ *
+ * @param $title Title
+ * @param $output OutputPage
+ * @param $request WebRequest
+ * @return bool true if the request is already executed
*/
- function initializeSpecialCases ( &$title, &$output, $request ) {
- global $wgRequest;
- wfProfileIn( 'MediaWiki::initializeSpecialCases' );
+ function initializeSpecialCases( &$title, &$output, $request ) {
+ wfProfileIn( __METHOD__ );
- $action = $this->getVal('Action');
- if( !$title or $title->getDBkey() == '' ) {
+ $action = $this->getVal( 'Action' );
+ if( !$title || $title->getDBkey() == '' ) {
$title = SpecialPage::getTitleFor( 'Badtitle' );
# Die now before we mess up $wgArticle and the skin stops working
throw new ErrorPageError( 'badtitle', 'badtitletext' );
@@ -155,20 +187,19 @@ class MediaWiki {
$title = SpecialPage::getTitleFor( 'Badtitle' );
throw new ErrorPageError( 'badtitle', 'badtitletext' );
}
- } else if ( ( $action == 'view' ) && !$wgRequest->wasPosted() &&
+ } 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();
// Redirect to canonical url, make it a 301 to allow caching
- global $wgUsePathInfo;
- if( $targetUrl == $wgRequest->getFullRequestURL() ) {
+ if( $targetUrl == $request->getFullRequestURL() ) {
$message = "Redirect loop detected!\n\n" .
"This means the wiki got confused about what page was " .
"requested; this sometimes happens when moving a wiki " .
"to a new server or changing the server configuration.\n\n";
- if( $wgUsePathInfo ) {
+ if( $this->getVal( 'UsePathInfo' ) ) {
$message .= "The wiki is trying to interpret the page " .
"title from the URL path portion (PATH_INFO), which " .
"sometimes fails depending on the web server. Try " .
@@ -186,44 +217,41 @@ class MediaWiki {
return false;
} else {
$output->setSquidMaxage( 1200 );
- $output->redirect( $targetUrl, '301');
+ $output->redirect( $targetUrl, '301' );
}
} else if ( NS_SPECIAL == $title->getNamespace() ) {
/* actions that need to be made when we have a special pages */
SpecialPage::executePath( $title );
} else {
/* No match to special cases */
- wfProfileOut( 'MediaWiki::initializeSpecialCases' );
+ wfProfileOut( __METHOD__ );
return false;
}
/* Did match a special case */
- wfProfileOut( 'MediaWiki::initializeSpecialCases' );
+ wfProfileOut( __METHOD__ );
return true;
}
/**
* Create an Article object of the appropriate class for the given page.
- * @param Title $title
- * @return Article
+ *
+ * @param $title Title
+ * @return Article object
*/
- static function articleFromTitle( $title ) {
- $article = null;
- wfRunHooks('ArticleFromTitle', array( &$title, &$article ) );
- if ( $article ) {
- return $article;
- }
-
+ static function articleFromTitle( &$title ) {
if( NS_MEDIA == $title->getNamespace() ) {
// FIXME: where should this go?
$title = Title::makeTitle( NS_IMAGE, $title->getDBkey() );
}
+ $article = null;
+ wfRunHooks( 'ArticleFromTitle', array( &$title, &$article ) );
+ if( $article ) {
+ return $article;
+ }
+
switch( $title->getNamespace() ) {
case NS_IMAGE:
- $file = wfFindFile( $title );
- if( $file && $file->getRedirected() ) {
- return new Article( $title );
- }
return new ImagePage( $title );
case NS_CATEGORY:
return new CategoryPage( $title );
@@ -235,83 +263,97 @@ class MediaWiki {
/**
* Initialize the object to be known as $wgArticle for "standard" actions
* Create an Article object for the page, following redirects if needed.
- * @param Title $title
- * @param Request $request
- * @param string $action
+ *
+ * @param $title Title ($wgTitle)
+ * @param $request WebRequest
* @return mixed an Article, or a string to redirect to another URL
*/
- function initializeArticle( $title, $request ) {
- global $wgTitle;
- wfProfileIn( 'MediaWiki::initializeArticle' );
-
- $action = $this->getVal('Action');
- $article = $this->articleFromTitle( $title );
+ function initializeArticle( &$title, $request ) {
+ wfProfileIn( __METHOD__ );
+ $action = $this->getVal( 'action' );
+ $article = self::articleFromTitle( $title );
+
+ wfDebug("Article: ".$title->getPrefixedText()."\n");
+
// Namespace might change when using redirects
- if( ( $action == 'view' || $action == 'render' ) && !$request->getVal( 'oldid' ) &&
- $request->getVal( 'redirect' ) != 'no' &&
- !( $wgTitle->getNamespace() == NS_IMAGE && wfFindFile( $wgTitle->getText() ) ) ) {
-
- $dbr = wfGetDB(DB_SLAVE);
- $article->loadPageData($article->pageDataFromTitle($dbr, $title));
-
- /* Follow redirects only for... redirects */
- if ($article->mIsRedirect) {
- $target = $article->followRedirect();
+ // Check for redirects ...
+ $file = $title->getNamespace() == NS_IMAGE ? $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() ) ) {
+
+ # 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 ) );
+
+ // Follow redirects only for... redirects
+ if( !$ignoreRedirect && $article->isRedirect() ) {
+ # Is the target already set by an extension?
+ $target = $target ? $target : $article->followRedirect();
if( is_string( $target ) ) {
- global $wgDisableHardRedirects;
- if( !$wgDisableHardRedirects ) {
+ if( !$this->getVal( 'DisableHardRedirects' ) ) {
// we'll need to redirect
return $target;
}
}
+
if( is_object( $target ) ) {
- /* Rewrite environment to redirected article */
- $rarticle = $this->articleFromTitle($target);
- $rarticle->loadPageData($rarticle->pageDataFromTitle($dbr,$target));
- if ($rarticle->mTitle->mArticleID) {
+ // Rewrite environment to redirected article
+ $rarticle = self::articleFromTitle( $target );
+ $rarticle->loadPageData( $rarticle->pageDataFromTitle( $dbr, $target ) );
+ if ( $rarticle->exists() || ( is_object( $file ) && !$file->isLocal() ) ) {
+ $rarticle->setRedirectedFrom( $title );
$article = $rarticle;
- $wgTitle = $target;
- $article->setRedirectedFrom( $title );
- } else {
- $wgTitle = $title;
+ $title = $target;
}
}
} else {
- $wgTitle = $article->mTitle;
+ $title = $article->getTitle();
}
}
- wfProfileOut( 'MediaWiki::initializeArticle' );
+ wfProfileOut( __METHOD__ );
return $article;
}
/**
- * Cleaning up by doing deferred updates, calling loadbalancer and doing the output
+ * Cleaning up by doing deferred updates, calling LBFactory and doing the output
+ *
+ * @param $deferredUpdates array of updates to do
+ * @param $output OutputPage
*/
- function finalCleanup ( &$deferredUpdates, &$loadBalancer, &$output ) {
- wfProfileIn( 'MediaWiki::finalCleanup' );
+ function finalCleanup ( &$deferredUpdates, &$output ) {
+ wfProfileIn( __METHOD__ );
$this->doUpdates( $deferredUpdates );
$this->doJobs();
- $loadBalancer->saveMasterPos();
# Now commit any transactions, so that unreported errors after output() don't roll back the whole thing
- $loadBalancer->commitMasterChanges();
+ $factory = wfGetLBFactory();
+ $factory->shutdown();
$output->output();
- wfProfileOut( 'MediaWiki::finalCleanup' );
+ wfProfileOut( __METHOD__ );
}
/**
- * Deferred updates aren't really deferred anymore. It's important to report errors to the
- * user, and that means doing this before OutputPage::output(). Note that for page saves,
- * the client will wait until the script exits anyway before following the redirect.
+ * Deferred updates aren't really deferred anymore. It's important to report
+ * errors to the user, and that means doing this before OutputPage::output().
+ * Note that for page saves, the client will wait until the script exits
+ * anyway before following the redirect.
+ *
+ * @param $updates array of objects that hold an update to do
*/
- function doUpdates ( &$updates ) {
- wfProfileIn( 'MediaWiki::doUpdates' );
+ function doUpdates( &$updates ) {
+ wfProfileIn( __METHOD__ );
/* No need to get master connections in case of empty updates array */
if (!$updates) {
- wfProfileOut('MediaWiki::doUpdates');
+ wfProfileOut( __METHOD__ );
return;
}
-
+
$dbw = wfGetDB( DB_MASTER );
foreach( $updates as $up ) {
$up->doUpdate();
@@ -321,29 +363,29 @@ class MediaWiki {
$dbw->commit();
}
}
- wfProfileOut( 'MediaWiki::doUpdates' );
+ wfProfileOut( __METHOD__ );
}
/**
* Do a job from the job queue
*/
function doJobs() {
- global $wgJobRunRate;
+ $jobRunRate = $this->getVal( 'JobRunRate' );
- if ( $wgJobRunRate <= 0 || wfReadOnly() ) {
+ if ( $jobRunRate <= 0 || wfReadOnly() ) {
return;
}
- if ( $wgJobRunRate < 1 ) {
+ if ( $jobRunRate < 1 ) {
$max = mt_getrandmax();
- if ( mt_rand( 0, $max ) > $max * $wgJobRunRate ) {
+ if ( mt_rand( 0, $max ) > $max * $jobRunRate ) {
return;
}
$n = 1;
} else {
- $n = intval( $wgJobRunRate );
+ $n = intval( $jobRunRate );
}
- while ( $n-- && false != ($job = Job::pop())) {
+ while ( $n-- && false != ( $job = Job::pop() ) ) {
$output = $job->toString() . "\n";
$t = -wfTime();
$success = $job->run();
@@ -361,25 +403,30 @@ class MediaWiki {
/**
* Ends this task peacefully
*/
- function restInPeace ( &$loadBalancer ) {
+ function restInPeace() {
wfLogProfilingData();
wfDebug( "Request ended normally\n" );
}
/**
* Perform one of the "standard" actions
+ *
+ * @param $output OutputPage
+ * @param $article Article
+ * @param $title Title
+ * @param $user User
+ * @param $request WebRequest
*/
function performAction( &$output, &$article, &$title, &$user, &$request ) {
+ wfProfileIn( __METHOD__ );
- wfProfileIn( 'MediaWiki::performAction' );
-
- if ( !wfRunHooks('MediaWikiPerformAction', array($output, $article, $title, $user, $request)) ) {
- wfProfileOut( 'MediaWiki::performAction' );
+ if ( !wfRunHooks( 'MediaWikiPerformAction', array( $output, $article, $title, $user, $request, $this ) ) ) {
+ wfProfileOut( __METHOD__ );
return;
}
- $action = $this->getVal('Action');
- if( in_array( $action, $this->getVal('DisabledActions',array()) ) ) {
+ $action = $this->getVal( 'Action' );
+ if( in_array( $action, $this->getVal( 'DisabledActions', array() ) ) ) {
/* No such action; this will switch to the default case */
$action = 'nosuchaction';
}
@@ -433,6 +480,7 @@ class MediaWiki {
}
/* Continue... */
case 'edit':
+ case 'editredlink':
if( wfRunHooks( 'CustomEditor', array( $article, $user ) ) ) {
$internal = $request->getVal( 'internaledit' );
$external = $request->getVal( 'externaledit' );
@@ -450,8 +498,7 @@ class MediaWiki {
}
break;
case 'history':
- global $wgRequest;
- if( $wgRequest->getFullRequestURL() == $title->getInternalURL( 'action=history' ) ) {
+ if( $request->getFullRequestURL() == $title->getInternalURL( 'action=history' ) ) {
$output->setSquidMaxage( $this->getVal( 'SquidMaxage' ) );
}
$history = new PageHistory( $article );
@@ -466,10 +513,8 @@ class MediaWiki {
$output->showErrorPage( 'nosuchaction', 'nosuchactiontext' );
}
}
- wfProfileOut( 'MediaWiki::performAction' );
+ wfProfileOut( __METHOD__ );
}
}; /* End of class MediaWiki */
-
-
diff --git a/includes/WikiError.php b/includes/WikiError.php
index b155f9bf..c5082004 100644
--- a/includes/WikiError.php
+++ b/includes/WikiError.php
@@ -24,11 +24,11 @@
/**
* Since PHP4 doesn't have exceptions, here's some error objects
* loosely modeled on the standard PEAR_Error model...
- * @addtogroup Exception
+ * @ingroup Exception
*/
class WikiError {
/**
- * @param string $message
+ * @param $message string
*/
function __construct( $message ) {
$this->mMessage = $message;
@@ -54,9 +54,8 @@ class WikiError {
* Returns true if the given object is a WikiError-descended
* error object, false otherwise.
*
- * @param mixed $object
+ * @param $object mixed
* @return bool
- * @static
*/
public static function isError( $object ) {
return $object instanceof WikiError;
@@ -65,11 +64,11 @@ class WikiError {
/**
* Localized error message object
- * @addtogroup Exception
+ * @ingroup Exception
*/
class WikiErrorMsg extends WikiError {
/**
- * @param string $message Wiki message name
+ * @param $message String: wiki message name
* @param ... parameters to pass to wfMsg()
*/
function WikiErrorMsg( $message/*, ... */ ) {
@@ -81,12 +80,14 @@ class WikiErrorMsg extends WikiError {
/**
* @todo document
- * @addtogroup Exception
+ * @ingroup Exception
*/
class WikiXmlError extends WikiError {
/**
- * @param resource $parser
- * @param string $message
+ * @param $parser resource
+ * @param $message string
+ * @param $context
+ * @param $offset Int
*/
function WikiXmlError( $parser, $message = 'XML parsing error', $context = null, $offset = 0 ) {
$this->mXmlError = xml_get_error_code( $parser );
diff --git a/includes/Xml.php b/includes/Xml.php
index 6689a4a4..32a68251 100644
--- a/includes/Xml.php
+++ b/includes/Xml.php
@@ -11,12 +11,13 @@ class Xml {
* Strings are assumed to not contain XML-illegal characters; special
* characters (<, >, &) are escaped but illegals are not touched.
*
- * @param $element String:
+ * @param $element String: element name
* @param $attribs Array: Name=>value pairs. Values will be escaped.
* @param $contents String: NULL to make an open tag only; '' for a contentless closed tag (default)
+ * @param $allowShortTag Bool: whether '' in $contents will result in a contentless closed tag
* @return string
*/
- public static function element( $element, $attribs = null, $contents = '') {
+ public static function element( $element, $attribs = null, $contents = '', $allowShortTag = true ) {
$out = '<' . $element;
if( !is_null( $attribs ) ) {
$out .= self::expandAttributes( $attribs );
@@ -24,7 +25,7 @@ class Xml {
if( is_null( $contents ) ) {
$out .= '>';
} else {
- if( $contents === '' ) {
+ if( $allowShortTag && $contents === '' ) {
$out .= ' />';
} else {
$out .= '>' . htmlspecialchars( $contents ) . "</$element>";
@@ -40,7 +41,7 @@ class Xml {
* Return null if no attributes given.
* @param $attribs Array of attributes for an XML element
*/
- private static function expandAttributes( $attribs ) {
+ public static function expandAttributes( $attribs ) {
$out = '';
if( is_null( $attribs ) ) {
return null;
@@ -75,17 +76,32 @@ class Xml {
return self::element( $element, $attribs, $contents );
}
- /** This open an XML element */
+ /**
+ * This opens an XML element
+ *
+ * @param $element name of the element
+ * @param $attribs array of attributes, see Xml::expandAttributes()
+ * @return string
+ */
public static function openElement( $element, $attribs = null ) {
return '<' . $element . self::expandAttributes( $attribs ) . '>';
}
- // Shortcut
+ /**
+ * Shortcut to close an XML element
+ * @param $element element name
+ * @return string
+ */
public static function closeElement( $element ) { return "</$element>"; }
/**
- * Same as <link>element</link>, but does not escape contents. Handy when the
+ * Same as Xml::element(), but does not escape contents. Handy when the
* content you have is already valid xml.
+ *
+ * @param $element element name
+ * @param $attribs array of attributes
+ * @param $contents content of the element
+ * @return string
*/
public static function tags( $element, $attribs = null, $contents ) {
return self::openElement( $element, $attribs ) . $contents . "</$element>";
@@ -94,16 +110,17 @@ class Xml {
/**
* Build a drop-down box for selecting a namespace
*
- * @param mixed $selected Namespace which should be pre-selected
- * @param mixed $all Value of an item denoting all namespaces, or null to omit
- * @param bool $hidden Include hidden namespaces? [WTF? --RC]
+ * @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
* @return string
*/
public static function namespaceSelector( $selected = '', $all = null, $hidden = false, $element_name = 'namespace' ) {
global $wgContLang;
$namespaces = $wgContLang->getFormattedNamespaces();
$options = array();
-
+
// Godawful hack... we'll be frequently passed selected namespaces
// as strings since PHP is such a shithole.
// But we also don't want blanks and nulls and "all"s matching 0,
@@ -111,7 +128,7 @@ class Xml {
if( preg_match( '/^\d+$/', $selected ) ) {
$selected = intval( $selected );
}
-
+
if( !is_null( $all ) )
$namespaces = array( $all => wfMsg( 'namespacesall' ) ) + $namespaces;
foreach( $namespaces as $index => $name ) {
@@ -121,7 +138,7 @@ class Xml {
$name = wfMsg( 'blanknamespace' );
$options[] = self::option( $name, $index, $index === $selected );
}
-
+
return Xml::openElement( 'select', array( 'id' => 'namespace', 'name' => $element_name,
'class' => 'namespaceselector' ) )
. "\n"
@@ -129,32 +146,32 @@ class Xml {
. "\n"
. Xml::closeElement( 'select' );
}
-
+
/**
- * Create a date selector
- *
- * @param $selected Mixed: the month which should be selected, default ''
- * @param $allmonths String: value of a special item denoting all month. Null to not include (default)
- * @param string $id Element identifier
- * @return String: Html string containing the month selector
- */
- public static function monthSelector( $selected = '', $allmonths = null, $id = 'month' ) {
- global $wgLang;
- $options = array();
- if( is_null( $selected ) )
- $selected = '';
- if( !is_null( $allmonths ) )
- $options[] = self::option( wfMsg( 'monthsall' ), $allmonths, $selected === $allmonths );
- for( $i = 1; $i < 13; $i++ )
- $options[] = self::option( $wgLang->getMonthName( $i ), $i, $selected === $i );
- return self::openElement( 'select', array( 'id' => $id, 'name' => 'month' ) )
- . implode( "\n", $options )
- . self::closeElement( 'select' );
+ * Create a date selector
+ *
+ * @param $selected Mixed: the month which should be selected, default ''
+ * @param $allmonths String: value of a special item denoting all month. Null to not include (default)
+ * @param $id String: Element identifier
+ * @return String: Html string containing the month selector
+ */
+ public static function monthSelector( $selected = '', $allmonths = null, $id = 'month' ) {
+ global $wgLang;
+ $options = array();
+ if( is_null( $selected ) )
+ $selected = '';
+ if( !is_null( $allmonths ) )
+ $options[] = self::option( wfMsg( 'monthsall' ), $allmonths, $selected === $allmonths );
+ for( $i = 1; $i < 13; $i++ )
+ $options[] = self::option( $wgLang->getMonthName( $i ), $i, $selected === $i );
+ return self::openElement( 'select', array( 'id' => $id, 'name' => 'month', 'class' => 'mw-month-selector' ) )
+ . implode( "\n", $options )
+ . self::closeElement( 'select' );
}
/**
*
- * @param $language The language code of the selected language
+ * @param $selected The language code of the selected language
* @param $customisedOnly If true only languages which have some content are listed
* @return array of label and select
*/
@@ -191,12 +208,35 @@ class Xml {
}
+ /**
+ * Shortcut to make a span element
+ * @param $text content of the element, will be escaped
+ * @param $class class name of the span element
+ * @param $attribs other attributes
+ * @return string
+ */
public static function span( $text, $class, $attribs=array() ) {
return self::element( 'span', array( 'class' => $class ) + $attribs, $text );
}
/**
+ * Shortcut to make a specific element with a class attribute
+ * @param $text content of the element, will be escaped
+ * @param $class class name of the span element
+ * @param $tag element name
+ * @param $attribs other attributes
+ * @return string
+ */
+ public static function wrapClass( $text, $class, $tag='span', $attribs=array() ) {
+ return self::tags( $tag, array( 'class' => $class ) + $attribs, $text );
+ }
+
+ /**
* Convenience function to build an HTML text input field
+ * @param $name value of the name attribute
+ * @param $size value of the size attribute
+ * @param $value value of the value attribute
+ * @param $attribs other attributes
* @return string HTML
*/
public static function input( $name, $size=false, $value=false, $attribs=array() ) {
@@ -208,6 +248,10 @@ class Xml {
/**
* Convenience function to build an HTML password input field
+ * @param $name value of the name attribute
+ * @param $size value of the size attribute
+ * @param $value value of the value attribute
+ * @param $attribs other attributes
* @return string HTML
*/
public static function password( $name, $size=false, $value=false, $attribs=array() ) {
@@ -224,6 +268,9 @@ class Xml {
/**
* Convenience function to build an HTML checkbox
+ * @param $name value of the name attribute
+ * @param $checked Whether the checkbox is checked or not
+ * @param $attribs other attributes
* @return string HTML
*/
public static function check( $name, $checked=false, $attribs=array() ) {
@@ -238,6 +285,10 @@ class Xml {
/**
* Convenience function to build an HTML radio button
+ * @param $name value of the name attribute
+ * @param $value value of the value attribute
+ * @param $checked Whether the checkbox is checked or not
+ * @param $attribs other attributes
* @return string HTML
*/
public static function radio( $name, $value, $checked=false, $attribs=array() ) {
@@ -249,6 +300,8 @@ class Xml {
/**
* Convenience function to build an HTML form label
+ * @param $label text of the label
+ * @param $id
* @return string HTML
*/
public static function label( $label, $id ) {
@@ -257,12 +310,27 @@ class Xml {
/**
* Convenience function to build an HTML text input field with a label
+ * @param $label text of the label
+ * @param $name value of the name attribute
+ * @param $id id of the input
+ * @param $size value of the size attribute
+ * @param $value value of the value attribute
+ * @param $attribs other attributes
* @return string HTML
*/
public static function inputLabel( $label, $name, $id, $size=false, $value=false, $attribs=array() ) {
- return Xml::label( $label, $id ) .
- '&nbsp;' .
- self::input( $name, $size, $value, array( 'id' => $id ) + $attribs );
+ list( $label, $input ) = self::inputLabelSep( $label, $name, $id, $size, $value, $attribs );
+ return $label . '&nbsp;' . $input;
+ }
+
+ /**
+ * Same as Xml::inputLabel() but return input and label in an array
+ */
+ public static function inputLabelSep( $label, $name, $id, $size=false, $value=false, $attribs=array() ) {
+ return array(
+ Xml::label( $label, $id ),
+ self::input( $name, $size, $value, array( 'id' => $id ) + $attribs )
+ );
}
/**
@@ -297,9 +365,8 @@ class Xml {
/**
* Convenience function to build an HTML hidden form field.
- * @todo Document $name parameter.
- * @param $name FIXME
- * @param $value String: label text for the button
+ * @param $name String: name attribute for the field
+ * @param $value String: value for the hidden field
* @param $attribs Array: optional custom attributes
* @return string HTML
*/
@@ -331,20 +398,21 @@ class Xml {
/**
* Build a drop-down box from a textual list.
- *
- * @param mixed $name Name and id for the drop-down
- * @param mixed $class CSS classes for the drop-down
- * @param mixed $other Text for the "Other reasons" option
- * @param mixed $list Correctly formatted text to be used to generate the options
- * @param mixed $selected Option which should be pre-selected
+ *
+ * @param $name Mixed: Name and id for the drop-down
+ * @param $class Mixed: CSS classes for the drop-down
+ * @param $other Mixed: Text for the "Other reasons" option
+ * @param $list Mixed: Correctly formatted text to be used to generate the options
+ * @param $selected Mixed: Option which should be pre-selected
+ * @param $tabindex Mixed: Value of the tabindex attribute
* @return string
*/
public static function listDropDown( $name= '', $list = '', $other = '', $selected = '', $class = '', $tabindex = Null ) {
$options = '';
$optgroup = false;
-
+
$options = self::option( $other, 'other', $selected === 'other' );
-
+
foreach ( explode( "\n", $list ) as $option) {
$value = trim( $option );
if ( $value == '' ) {
@@ -367,7 +435,7 @@ class Xml {
}
}
if( $optgroup ) $options .= self::closeElement('optgroup');
-
+
$attribs = array();
if( $name ) {
$attribs['id'] = $name;
@@ -387,12 +455,51 @@ class Xml {
}
/**
+ * Shortcut for creating fieldsets.
+ *
+ * @param $legend Legend of the fieldset. If evaluates to false, legend is not added.
+ * @param $content Pre-escaped content for the fieldset. If false, only open fieldset is returned.
+ * @param $attribs Any attributes to fieldset-element.
+ */
+ public static function fieldset( $legend = false, $content = false, $attribs = array() ) {
+ $s = Xml::openElement( 'fieldset', $attribs ) . "\n";
+ if ( $legend ) {
+ $s .= Xml::element( 'legend', null, $legend ) . "\n";
+ }
+ if ( $content !== false ) {
+ $s .= $content . "\n";
+ $s .= Xml::closeElement( 'fieldset' ) . "\n";
+ }
+
+ return $s;
+ }
+
+ /**
+ * Shortcut for creating textareas.
+ *
+ * @param $name The 'name' for the textarea
+ * @param $content Content for the textarea
+ * @param $cols The number of columns for the textarea
+ * @param $rows The number of rows for the textarea
+ * @param $attribs Any other attributes for the textarea
+ */
+ public static function textarea( $name, $content, $cols = 40, $rows = 5, $attribs = array() ) {
+ return self::element( 'textarea',
+ array( 'name' => $name,
+ 'id' => $name,
+ 'cols' => $cols,
+ 'rows' => $rows
+ ) + $attribs,
+ $content, false );
+ }
+
+ /**
* Returns an escaped string suitable for inclusion in a string literal
* for JavaScript source code.
* Illegal control characters are assumed not to be present.
*
- * @param string $string
- * @return string
+ * @param $string String to escape
+ * @return String
*/
public static function escapeJsString( $string ) {
// See ECMA 262 section 7.8.4 for string literal format
@@ -407,9 +514,9 @@ class Xml {
"<" => "\\x3c",
">" => "\\x3e",
- # To avoid any complaints about bad entity refs
+ # To avoid any complaints about bad entity refs
"&" => "\\x26",
-
+
# Work around https://bugzilla.mozilla.org/show_bug.cgi?id=274152
# Encode certain Unicode formatting chars so affected
# versions of Gecko don't misinterpret our strings;
@@ -422,8 +529,8 @@ class Xml {
/**
* Encode a variable of unknown type to JavaScript.
- * Arrays are converted to JS arrays, objects are converted to JS associative
- * arrays (objects). So cast your PHP associative arrays to objects before
+ * Arrays are converted to JS arrays, objects are converted to JS associative
+ * arrays (objects). So cast your PHP associative arrays to objects before
* passing them to here.
*/
public static function encodeJsVar( $value ) {
@@ -448,7 +555,7 @@ class Xml {
if ( $s != '{' ) {
$s .= ', ';
}
- $s .= '"' . self::escapeJsString( $name ) . '": ' .
+ $s .= '"' . self::escapeJsString( $name ) . '": ' .
self::encodeJsVar( $elt );
}
$s .= '}';
@@ -457,7 +564,7 @@ class Xml {
}
return $s;
}
-
+
/**
* Check if a string is well-formed XML.
@@ -516,5 +623,63 @@ class Xml {
array( '&quot;', '&gt;', '&lt;' ),
$in );
}
+
+ /**
+ * Generate a form (without the opening form element).
+ * Output optionally includes a submit button.
+ * @param $fields Associative array, key is message corresponding to a description for the field (colon is in the message), value is appropriate input.
+ * @param $submitLabel A message containing a label for the submit button.
+ * @return string HTML form.
+ */
+ public static function buildForm( $fields, $submitLabel = null ) {
+ $form = '';
+ $form .= "<table><tbody>";
+
+ foreach( $fields as $labelmsg => $input ) {
+ $id = "mw-$labelmsg";
+
+ $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::closeElement( 'tr' );
+ }
+
+ $form .= "</tbody></table>";
+
+ if ($submitLabel) {
+ $form .= Xml::submitButton( wfMsg($submitLabel) );
+ }
+
+ return $form;
+ }
}
+class XmlSelect {
+ protected $options = array();
+ protected $default = false;
+ protected $attributes = array();
+
+ public function __construct( $name = false, $id = false, $default = false ) {
+ if ( $name ) $this->setAttribute( 'name', $name );
+ if ( $id ) $this->setAttribute( 'id', $id );
+ if ( $default ) $this->default = $default;
+ }
+
+ public function setDefault( $default ) {
+ $this->default = $default;
+ }
+
+ public function setAttribute( $name, $value ) {
+ $this->attributes[$name] = $value;
+ }
+
+ public function addOption( $name, $value = false ) {
+ $value = $value ? $value : $name;
+ $this->options[] = Xml::option( $name, $value, $value === $this->default );
+ }
+
+ public function getHTML() {
+ 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 2e86aa7d..bc18a2cd 100644
--- a/includes/XmlFunctions.php
+++ b/includes/XmlFunctions.php
@@ -59,4 +59,8 @@ function wfIsWellFormedXml( $text ) {
}
function wfIsWellFormedXmlFragment( $text ) {
return Xml::isWellFormedXmlFragment( $text );
-} \ No newline at end of file
+}
+
+function wfBuildForm( $fields, $submitLabel ) {
+ return Xml::buildForm( $fields, $submitLabel );
+}
diff --git a/includes/XmlTypeCheck.php b/includes/XmlTypeCheck.php
index 639d1f85..09b8c20a 100644
--- a/includes/XmlTypeCheck.php
+++ b/includes/XmlTypeCheck.php
@@ -6,16 +6,16 @@ class XmlTypeCheck {
* well-formed XML. Note that this doesn't check schema validity.
*/
public $wellFormed = false;
-
+
/**
* Name of the document's root element, including any namespace
* as an expanded URL.
*/
public $rootElement = '';
-
+
private $softNamespaces;
private $namespaces = array();
-
+
/**
* @param $file string filename
* @param $softNamespaces bool
@@ -28,17 +28,17 @@ class XmlTypeCheck {
$this->softNamespaces = $softNamespaces;
$this->run( $file );
}
-
+
private function run( $fname ) {
if( $this->softNamespaces ) {
$parser = xml_parser_create( 'UTF-8' );
} else {
$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_element_handler( $parser, array( $this, 'elementOpen' ), false );
$file = fopen( $fname, "rb" );
@@ -52,9 +52,9 @@ class XmlTypeCheck {
return;
}
} while( !feof( $file ) );
-
+
$this->wellFormed = true;
-
+
fclose( $file );
xml_parser_free( $parser );
}
@@ -70,14 +70,14 @@ class XmlTypeCheck {
$this->namespaces[substr( $attrib, strlen( 'xmlns:' ) )] = $val;
}
}
-
+
if( strpos( $name, ':' ) === false ) {
$ns = '';
$subname = $name;
} else {
list( $ns, $subname ) = explode( ':', $name, 2 );
}
-
+
if( isset( $this->namespaces[$ns] ) ) {
$name = $this->namespaces[$ns] . ':' . $subname;
} else {
@@ -85,7 +85,7 @@ class XmlTypeCheck {
// But..... we'll just let it slide in soft mode.
}
}
-
+
// We only need the first open element
$this->rootElement = $name;
xml_set_element_handler( $parser, false, false );
diff --git a/includes/ZhClient.php b/includes/ZhClient.php
index 7f2c5cbf..61faa8df 100644
--- a/includes/ZhClient.php
+++ b/includes/ZhClient.php
@@ -1,6 +1,4 @@
<?php
-/**
- */
/**
* Client for querying zhdaemon
@@ -22,8 +20,6 @@ class ZhClient {
/**
* Check if connection to zhdaemon is successful
- *
- * @access public
*/
function isconnected() {
return $this->mConnected;
@@ -80,10 +76,9 @@ class ZhClient {
/**
* Convert the input to a different language variant
*
- * @param string $text input text
- * @param string $tolang language variant
+ * @param $text string: input text
+ * @param $tolang string: language variant
* @return string the converted text
- * @access public
*/
function convert($text, $tolang) {
$len = strlen($text);
@@ -97,9 +92,8 @@ class ZhClient {
/**
* Convert the input to all possible variants
*
- * @param string $text input text
+ * @param $text string: input text
* @return array langcode => converted_string
- * @access public
*/
function convertToAllVariants($text) {
$len = strlen($text);
@@ -121,9 +115,8 @@ class ZhClient {
/**
* Perform word segmentation
*
- * @param string $text input text
+ * @param $text string: input text
* @return string segmented text
- * @access public
*/
function segment($text) {
$len = strlen($text);
@@ -137,8 +130,6 @@ class ZhClient {
/**
* Close the connection
- *
- * @access public
*/
function close() {
fclose($this->mFP);
diff --git a/includes/ZhConversion.php b/includes/ZhConversion.php
index 62bebb4e..1cae8463 100644
--- a/includes/ZhConversion.php
+++ b/includes/ZhConversion.php
@@ -8,100 +8,38 @@
$zh2Hant = array(
"画"=>"畫",
-"板"=>"板",
-"表"=>"表",
-"才"=>"才",
-"丑"=>"醜",
-"出"=>"出",
-"淀"=>"澱",
-"冬"=>"冬",
-"范"=>"範",
"丰"=>"豐",
-"刮"=>"刮",
-"后"=>"後",
-"胡"=>"胡",
-"回"=>"回",
-"伙"=>"夥",
-"姜"=>"薑",
-"借"=>"借",
-"克"=>"克",
-"困"=>"困",
-"漓"=>"漓",
-"里"=>"里",
"帘"=>"簾",
-"霉"=>"霉",
-"面"=>"面",
-"蔑"=>"蔑",
-"千"=>"千",
-"秋"=>"秋",
-"松"=>"松",
-"咸"=>"咸",
-"向"=>"向",
-"余"=>"餘",
-"郁"=>"鬱",
-"御"=>"御",
"愿"=>"願",
"云"=>"雲",
-"芸"=>"芸",
-"沄"=>"沄",
-"致"=>"致",
-"制"=>"制",
-"朱"=>"朱",
"筑"=>"築",
-"准"=>"準",
"厂"=>"廠",
"广"=>"廣",
-"辟"=>"闢",
"别"=>"別",
-"卜"=>"卜",
-"沈"=>"沈",
"冲"=>"沖",
"种"=>"種",
"虫"=>"蟲",
"担"=>"擔",
"党"=>"黨",
-"斗"=>"鬥",
"儿"=>"兒",
-"干"=>"乾",
-"谷"=>"谷",
"柜"=>"櫃",
-"合"=>"合",
-"划"=>"劃",
"坏"=>"壞",
"几"=>"幾",
-"系"=>"系",
-"家"=>"家",
"价"=>"價",
"据"=>"據",
-"卷"=>"捲",
"适"=>"適",
"蜡"=>"蠟",
"腊"=>"臘",
-"了"=>"了",
-"累"=>"累",
-"么"=>"麽",
-"蒙"=>"蒙",
"万"=>"萬",
"宁"=>"寧",
-"朴"=>"樸",
"苹"=>"蘋",
-"仆"=>"僕",
-"曲"=>"曲",
"确"=>"確",
-"舍"=>"舍",
"胜"=>"勝",
"术"=>"術",
-"台"=>"台",
"体"=>"體",
"涂"=>"塗",
"叶"=>"葉",
-"吁"=>"吁",
-"旋"=>"旋",
-"佣"=>"傭",
"与"=>"與",
-"折"=>"折",
-"征"=>"徵",
-"症"=>"症",
"恶"=>"惡",
"发"=>"發",
"复"=>"復",
@@ -110,7 +48,7 @@ $zh2Hant = array(
"饥"=>"飢",
"尽"=>"盡",
"历"=>"歷",
-"卤"=>"滷",
+"卤"=>"鹵",
"弥"=>"彌",
"签"=>"簽",
"纤"=>"纖",
@@ -119,17 +57,9 @@ $zh2Hant = array(
"团"=>"團",
"须"=>"須",
"脏"=>"臟",
-"只"=>"只",
-"钟"=>"鐘",
-"药"=>"藥",
-"同"=>"同",
-"志"=>"志",
-"杯"=>"杯",
-"岳"=>"岳",
-"布"=>"布",
+"钟"=>"鍾",
+"药"=>"葯",
"当"=>"當",
-"吊"=>"弔",
-"仇"=>"仇",
"蕴"=>"蘊",
"线"=>"線",
"为"=>"為",
@@ -149,10 +79,10 @@ $zh2Hant = array(
"极"=>"極",
"沩"=>"溈",
"瘘"=>"瘺",
-"硷"=>"鹼",
+"硷"=>"礆",
"竖"=>"豎",
"绝"=>"絕",
-"绣"=>"繡",
+"绣"=>"綉",
"绦"=>"絛",
"绱"=>"緔",
"绷"=>"綳",
@@ -167,7 +97,6 @@ $zh2Hant = array(
"赍"=>"齎",
"赝"=>"贗",
"酝"=>"醞",
-"采"=>"採",
"钩"=>"鉤",
"钵"=>"缽",
"锈"=>"銹",
@@ -184,6 +113,8 @@ $zh2Hant = array(
"鳄"=>"鱷",
"鸡"=>"雞",
"鹚"=>"鶿",
+"仑"=>"侖",
+"赞"=>"贊",
"荡"=>"盪",
"锤"=>"錘",
"㟆"=>"㠏",
@@ -211,7 +142,6 @@ $zh2Hant = array(
"买"=>"買",
"乱"=>"亂",
"争"=>"爭",
-"于"=>"於",
"亏"=>"虧",
"亚"=>"亞",
"亩"=>"畝",
@@ -221,7 +151,6 @@ $zh2Hant = array(
"亿"=>"億",
"仅"=>"僅",
"从"=>"從",
-"仑"=>"侖",
"仓"=>"倉",
"仪"=>"儀",
"们"=>"們",
@@ -342,7 +271,6 @@ $zh2Hant = array(
"厌"=>"厭",
"厍"=>"厙",
"厐"=>"龎",
-"厘"=>"釐",
"厢"=>"廂",
"厣"=>"厴",
"厦"=>"廈",
@@ -426,7 +354,6 @@ $zh2Hant = array(
"圣"=>"聖",
"圹"=>"壙",
"场"=>"場",
-"坂"=>"阪",
"块"=>"塊",
"坚"=>"堅",
"坜"=>"壢",
@@ -467,7 +394,6 @@ $zh2Hant = array(
"奂"=>"奐",
"奋"=>"奮",
"奥"=>"奧",
-"奸"=>"姦",
"妆"=>"妝",
"妇"=>"婦",
"妈"=>"媽",
@@ -563,7 +489,6 @@ $zh2Hant = array(
"帻"=>"幘",
"帼"=>"幗",
"幂"=>"冪",
-"庄"=>"莊",
"庆"=>"慶",
"庐"=>"廬",
"庑"=>"廡",
@@ -583,7 +508,6 @@ $zh2Hant = array(
"弹"=>"彈",
"强"=>"強",
"归"=>"歸",
-"彝"=>"彞",
"彦"=>"彥",
"彻"=>"徹",
"径"=>"徑",
@@ -678,7 +602,6 @@ $zh2Hant = array(
"挤"=>"擠",
"挥"=>"揮",
"挦"=>"撏",
-"挽"=>"輓",
"捝"=>"挩",
"捞"=>"撈",
"损"=>"損",
@@ -773,7 +696,6 @@ $zh2Hant = array(
"栏"=>"欄",
"树"=>"樹",
"栖"=>"棲",
-"栗"=>"慄",
"样"=>"樣",
"栾"=>"欒",
"桠"=>"椏",
@@ -853,7 +775,6 @@ $zh2Hant = array(
"沧"=>"滄",
"沪"=>"滬",
"泞"=>"濘",
-"注"=>"註",
"泪"=>"淚",
"泶"=>"澩",
"泷"=>"瀧",
@@ -936,7 +857,6 @@ $zh2Hant = array(
"灭"=>"滅",
"灯"=>"燈",
"灵"=>"靈",
-"灶"=>"竈",
"灾"=>"災",
"灿"=>"燦",
"炀"=>"煬",
@@ -1060,7 +980,6 @@ $zh2Hant = array(
"眍"=>"瞘",
"眦"=>"眥",
"眬"=>"矓",
-"着"=>"著",
"睁"=>"睜",
"睐"=>"睞",
"睑"=>"瞼",
@@ -1740,7 +1659,6 @@ $zh2Hant = array(
"赚"=>"賺",
"赛"=>"賽",
"赜"=>"賾",
-"赞"=>"贊",
"赟"=>"贇",
"赠"=>"贈",
"赡"=>"贍",
@@ -2609,148 +2527,517 @@ $zh2Hant = array(
"龛"=>"龕",
"龟"=>"龜",
-"BIG-" => "BIG-",
-"一伙" => "一伙",
+"0多只" => "0多隻",
+"0天后" => "0天後",
+"1天后" => "1天後",
+"2天后" => "2天後",
+"3天后" => "3天後",
+"4天后" => "4天後",
+"5天后" => "5天後",
+"6天后" => "6天後",
+"7天后" => "7天後",
+"8天后" => "8天後",
+"9天后" => "9天後",
+"一干二净" => "一乾二淨",
"一并" => "一併",
-"一准" => "一准",
-"一划" => "一划",
+"一前一后" => "一前一後",
+"一划" => "一劃",
+"一口钟" => "一口鐘",
"一地里" => "一地裡",
-"一干" => "一干",
+"一伙" => "一夥",
+"一天后" => "一天後",
+"一干人" => "一干人",
+"一别头" => "一彆頭",
"一树百获" => "一樹百穫",
-"一台" => "一臺",
+"一准" => "一準",
+"一争两丑" => "一爭兩醜",
+"一箭双雕" => "一箭雙鵰",
+"一扎" => "一紮",
"一冲" => "一衝",
+"一锅面" => "一鍋麵",
"一只" => "一隻",
"一发千钧" => "一髮千鈞",
-"一出" => "一齣",
+"一哄而散" => "一鬨而散",
+"丁丁当当" => "丁丁當當",
+"七划" => "七劃",
+"七天后" => "七天後",
+"七情六欲" => "七情六慾",
+"七扎" => "七紮",
"七只" => "七隻",
-"三元里" => "三元裡",
-"三国志" => "三國誌",
+"万俟" => "万俟",
+"万旗" => "万旗",
+"三天后" => "三天後",
+"三棱锥" => "三稜錐",
+"三扎" => "三紮",
+"三统历" => "三統曆",
"三复" => "三複",
"三只" => "三隻",
-"上吊" => "上吊",
-"上台" => "上臺",
-"下不了台" => "下不了臺",
-"下台" => "下臺",
-"下面" => "下麵",
-"不准" => "不准",
-"不吊" => "不吊",
+"三余" => "三餘",
+"上梁" => "上樑",
+"上签" => "上籤",
+"上药" => "上藥",
+"下于" => "下於",
+"下签" => "下籤",
+"下药" => "下藥",
+"下余" => "下餘",
+"不下于" => "不下於",
+"不亚于" => "不亞於",
+"不占" => "不佔",
+"不前不后" => "不前不後",
+"不可救药" => "不可救藥",
+"不同于" => "不同於",
+"不嫌母丑" => "不嫌母醜",
+"不寒而栗" => "不寒而慄",
+"不屑于" => "不屑於",
+"不干不净" => "不幹不淨",
+"不干性油" => "不幹性油",
+"不采" => "不採",
+"不断发" => "不斷發",
+"不准" => "不準",
+"不为牛后" => "不為牛後",
"不知就里" => "不知就裡",
"不知所云" => "不知所云",
+"不谷" => "不穀",
+"不绝于耳" => "不絕於耳",
+"不致于" => "不致於",
+"不良于行" => "不良於行",
+"不药而癒" => "不藥而癒",
+"不讬" => "不託",
+"不逊于" => "不遜於",
+"不丑" => "不醜",
"不锈钢" => "不鏽鋼",
-"丑剧" => "丑劇",
-"丑旦" => "丑旦",
-"丑角" => "丑角",
+"世界杯" => "世界盃",
+"丢丑" => "丟醜",
"并存着" => "並存著",
+"并于" => "並於",
+"并发动" => "並發動",
+"并发展" => "並發展",
+"并发现" => "並發現",
+"并发表" => "並發表",
+"中仑" => "中崙",
"中岳" => "中嶽",
-"中台医专" => "中臺醫專",
+"中于" => "中於",
+"中美发表" => "中美發表",
+"中药" => "中藥",
+"丰仪" => "丰儀",
"丰南" => "丰南",
"丰台" => "丰台",
"丰姿" => "丰姿",
+"丰度" => "丰度",
+"丰情" => "丰情",
+"丰标不凡" => "丰標不凡",
+"丰神" => "丰神",
+"丰茸" => "丰茸",
"丰采" => "丰采",
+"丰韵" => "丰韵",
"丰韵" => "丰韻",
+"丸药" => "丸藥",
+"丹药" => "丹藥",
+"主仆" => "主僕",
"主干" => "主幹",
-"么么唱唱" => "么么唱唱",
-"么儿" => "么兒",
-"么喝" => "么喝",
-"么妹" => "么妹",
-"么弟" => "么弟",
-"么爷" => "么爺",
+"么么小丑" => "么麼小丑",
+"之后" => "之後",
+"之于" => "之於",
+"之余" => "之餘",
"九世之雠" => "九世之讎",
+"九划" => "九劃",
+"九天后" => "九天後",
+"九谷" => "九穀",
+"九扎" => "九紮",
"九只" => "九隻",
+"乳臭未干" => "乳臭未乾",
+"干上" => "乾上",
+"干干" => "乾乾",
+"干干儿的" => "乾乾兒的",
+"干干净净" => "乾乾淨淨",
+"干了" => "乾了",
+"干井" => "乾井",
+"干个" => "乾個",
+"干儿" => "乾兒",
+"干儿子" => "乾兒子",
+"干冰" => "乾冰",
+"干冷" => "乾冷",
+"干刻版" => "乾刻版",
+"干剥剥" => "乾剝剝",
+"干卦" => "乾卦",
+"干吊着下巴" => "乾吊著下巴",
+"干和" => "乾和",
+"干咳" => "乾咳",
+"干咽" => "乾咽",
+"干哥" => "乾哥",
+"干哭" => "乾哭",
+"干唱" => "乾唱",
+"干啼" => "乾啼",
+"干乔" => "乾喬",
+"干呕" => "乾嘔",
+"干哕" => "乾噦",
+"干嚎" => "乾嚎",
+"干回付" => "乾回付",
+"干圆洁净" => "乾圓潔淨",
+"干地" => "乾地",
+"干坤" => "乾坤",
+"干坞" => "乾塢",
+"干女" => "乾女",
+"干女儿" => "乾女兒",
+"干奴才" => "乾奴才",
+"干妹" => "乾妹",
+"干姊" => "乾姊",
+"干娘" => "乾娘",
+"干妈" => "乾媽",
+"干子" => "乾子",
+"干季" => "乾季",
+"干尸" => "乾屍",
+"干屎橛" => "乾屎橛",
+"干巴" => "乾巴",
+"干巴巴" => "乾巴巴",
+"干式" => "乾式",
+"干弟" => "乾弟",
+"干得" => "乾得",
+"干急" => "乾急",
+"干性" => "乾性",
+"干打雷" => "乾打雷",
+"干折" => "乾折",
+"干掉" => "乾掉",
+"干撂台" => "乾撂台",
+"干撇下" => "乾撇下",
+"干擦" => "乾擦",
+"干支剌" => "乾支剌",
+"干支支" => "乾支支",
+"干敲梆子不卖油" => "乾敲梆子不賣油",
+"干料" => "乾料",
+"干旱" => "乾旱",
+"干暖" => "乾暖",
+"干材" => "乾材",
+"干村沙" => "乾村沙",
+"干杯" => "乾杯",
+"干果" => "乾果",
+"干枯" => "乾枯",
+"干柴" => "乾柴",
+"干柴烈火" => "乾柴烈火",
+"干梅" => "乾梅",
+"干死" => "乾死",
+"干池" => "乾池",
+"干没" => "乾沒",
+"干洗" => "乾洗",
+"干涸" => "乾涸",
+"干凉" => "乾涼",
+"干净" => "乾淨",
+"干渠" => "乾渠",
+"干渴" => "乾渴",
+"干沟" => "乾溝",
+"干漆" => "乾漆",
+"干涩" => "乾澀",
+"干湿" => "乾濕",
+"干熬" => "乾熬",
+"干热" => "乾熱",
+"干灯盏" => "乾燈盞",
+"干燥" => "乾燥",
+"干爸" => "乾爸",
+"干爹" => "乾爹",
+"干爽" => "乾爽",
+"干片" => "乾片",
+"干生受" => "乾生受",
+"干生子" => "乾生子",
+"干产" => "乾產",
+"干田" => "乾田",
+"干疥" => "乾疥",
+"干瘦" => "乾瘦",
+"干瘪" => "乾癟",
+"干癣" => "乾癬",
+"干白儿" => "乾白兒",
+"干的" => "乾的",
+"干眼" => "乾眼",
+"干眼病" => "乾眼病",
+"干瞪眼" => "乾瞪眼",
+"干礼" => "乾禮",
+"干稿" => "乾稿",
+"干笑" => "乾笑",
+"干等" => "乾等",
+"干篾片" => "乾篾片",
+"干粉" => "乾粉",
+"干粮" => "乾糧",
+"干结" => "乾結",
"干丝" => "乾絲",
+"干绷" => "乾繃",
+"干耗" => "乾耗",
+"干肉片" => "乾肉片",
+"干股" => "乾股",
+"干肥" => "乾肥",
+"干脆" => "乾脆",
+"干花" => "乾花",
+"干刍" => "乾芻",
+"干苔" => "乾苔",
+"干茨腊" => "乾茨臘",
+"干茶钱" => "乾茶錢",
+"干草" => "乾草",
+"干菜" => "乾菜",
+"干落" => "乾落",
+"干着" => "乾著",
"干着急" => "乾著急",
+"干姜" => "乾薑",
+"干薪" => "乾薪",
+"干虔" => "乾虔",
+"干号" => "乾號",
+"干衣" => "乾衣",
+"干裂" => "乾裂",
+"干亲" => "乾親",
+"干贝" => "乾貝",
+"干货" => "乾貨",
+"干躁" => "乾躁",
+"干逼" => "乾逼",
+"干酪" => "乾酪",
+"干酵母" => "乾酵母",
+"干醋" => "乾醋",
+"干量" => "乾量",
+"干阿奶" => "乾阿奶",
+"干隆" => "乾隆",
+"干雷" => "乾雷",
+"干电" => "乾電",
+"干电池" => "乾電池",
+"干霍乱" => "乾霍亂",
+"干颡" => "乾顙",
+"干台" => "乾颱",
+"干饭" => "乾飯",
+"干馆" => "乾館",
+"干糇" => "乾餱",
+"干馏" => "乾餾",
+"干鱼" => "乾魚",
+"干鲜" => "乾鮮",
+"干面" => "乾麵",
"乱发" => "亂髮",
+"乱哄" => "亂鬨",
+"乱哄不过来" => "亂鬨不過來",
+"事后" => "事後",
+"二不棱登" => "二不稜登",
+"二划" => "二劃",
+"二天后" => "二天後",
+"二缶钟惑" => "二缶鐘惑",
+"二里头" => "二里頭",
+"二只" => "二隻",
+"于余曲折" => "于餘曲折",
+"云乎" => "云乎",
"云云" => "云云",
+"云为" => "云為",
+"云然" => "云然",
"云尔" => "云爾",
+"互于" => "互於",
+"五划" => "五劃",
+"五天后" => "五天後",
"五岳" => "五嶽",
-"五斗柜" => "五斗櫃",
-"五斗橱" => "五斗櫥",
"五谷" => "五穀",
+"五扎" => "五紮",
"五行生克" => "五行生剋",
"五只" => "五隻",
"五出" => "五齣",
-"交卷" => "交卷",
+"井干摧败" => "井榦摧敗",
+"亚于" => "亞於",
+"交于" => "交於",
+"交讬" => "交託",
+"交游广阔" => "交遊廣闊",
+"交哄" => "交鬨",
+"亮丑" => "亮醜",
+"亮钟" => "亮鐘",
"人云亦云" => "人云亦云",
+"人参加" => "人參加",
+"人参展" => "人參展",
+"人参战" => "人參戰",
+"人参拜" => "人參拜",
+"人参政" => "人參政",
+"人参照" => "人參照",
+"人参看" => "人參看",
+"人参禅" => "人參禪",
+"人参考" => "人參考",
+"人参与" => "人參與",
+"人参见" => "人參見",
+"人参观" => "人參觀",
+"人参谋" => "人參謀",
+"人参议" => "人參議",
+"人参赞" => "人參贊",
+"人参透" => "人參透",
+"人参选" => "人參選",
+"人参酌" => "人參酌",
+"人参阅" => "人參閱",
+"人后" => "人後",
+"人欲" => "人慾",
"人物志" => "人物誌",
+"人参" => "人蔘",
"什锦面" => "什錦麵",
"什么" => "什麼",
-"仆倒" => "仆倒",
-"介系词" => "介係詞",
-"介系词" => "介繫詞",
+"今后" => "今後",
+"介于" => "介於",
+"付讬" => "付託",
+"仙药" => "仙藥",
+"以后" => "以後",
+"任教于" => "任教於",
+"任于" => "任於",
"仿制" => "仿製",
-"伙伕" => "伙伕",
-"伙伴" => "伙伴",
-"伙同" => "伙同",
-"伙夫" => "伙夫",
-"伙房" => "伙房",
-"伙计" => "伙計",
-"伙食" => "伙食",
-"布下" => "佈下",
-"布告" => "佈告",
-"布哨" => "佈哨",
-"布局" => "佈局",
-"布岗" => "佈崗",
-"布施" => "佈施",
-"布景" => "佈景",
-"布满" => "佈滿",
-"布线" => "佈線",
-"布置" => "佈置",
-"布署" => "佈署",
-"布道" => "佈道",
-"布达" => "佈達",
-"布防" => "佈防",
-"布阵" => "佈陣",
-"布雷" => "佈雷",
-"体育锻鍊" => "体育鍛鍊",
-"何干" => "何干",
-"作准" => "作准",
-"佣人" => "佣人",
-"佣工" => "佣工",
-"佣金" => "佣金",
+"企划" => "企劃",
+"伊府面" => "伊府麵",
+"伊斯兰教历" => "伊斯蘭教曆",
+"伊斯兰历" => "伊斯蘭曆",
+"伊郁" => "伊鬱",
+"伏几" => "伏几",
+"伙头" => "伙頭",
+"似于" => "似於",
+"但云" => "但云",
+"布于" => "佈於",
+"位于" => "位於",
+"低于" => "低於",
+"占上风" => "佔上風",
+"占下" => "佔下",
+"占了" => "佔了",
+"占位" => "佔位",
+"占住" => "佔住",
+"占占" => "佔佔",
+"占便宜" => "佔便宜",
+"占个" => "佔個",
+"占优势" => "佔優勢",
+"占先" => "佔先",
+"占光" => "佔光",
+"占到" => "佔到",
+"占去" => "佔去",
+"占取" => "佔取",
+"占在" => "佔在",
+"占地" => "佔地",
+"占多数" => "佔多數",
+"占好" => "佔好",
+"占得" => "佔得",
+"占掉" => "佔掉",
+"占据" => "佔據",
+"占有" => "佔有",
+"占满" => "佔滿",
+"占为" => "佔為",
+"占用" => "佔用",
+"占尽" => "佔盡",
+"占线" => "佔線",
+"占起" => "佔起",
+"占超过" => "佔超過",
+"占过" => "佔過",
+"占领" => "佔領",
+"余光中" => "余光中",
+"余光生" => "余光生",
+"佛罗棱萨" => "佛羅稜薩",
+"作奸犯科" => "作姦犯科",
+"作准" => "作準",
+"作庄" => "作莊",
+"你才子发昏" => "你纔子發昏",
+"并一不二" => "併一不二",
"并入" => "併入",
-"并列" => "併列",
+"并兼" => "併兼",
"并到" => "併到",
"并合" => "併合",
+"并名" => "併名",
"并吞" => "併吞",
-"并在" => "併在",
-"并成" => "併成",
-"并排" => "併排",
"并拢" => "併攏",
"并案" => "併案",
+"并流" => "併流",
+"并火" => "併火",
"并为" => "併為",
+"并产" => "併產",
+"并当" => "併當",
+"并叠" => "併疊",
"并发" => "併發",
"并科" => "併科",
+"并网" => "併網",
+"并线" => "併線",
+"并肩子" => "併肩子",
"并购" => "併購",
-"并进" => "併進",
+"并除" => "併除",
+"并骨" => "併骨",
+"来于" => "來於",
+"来自于" => "來自於",
"来复" => "來複",
+"侍仆" => "侍僕",
"供制" => "供製",
"依依不舍" => "依依不捨",
+"依讬" => "依託",
+"依附于" => "依附於",
+"侵占" => "侵佔",
"侵并" => "侵併",
-"便辟" => "便辟",
+"便于" => "便於",
+"便药" => "便藥",
"系数" => "係數",
"系为" => "係為",
"保险柜" => "保險柜",
-"信号台" => "信號臺",
-"修复" => "修複",
+"信讬" => "信託",
+"修改后" => "修改後",
"修胡刀" => "修鬍刀",
"俯冲" => "俯衝",
"个里" => "個裡",
+"幸免" => "倖免",
+"幸存" => "倖存",
+"幸幸" => "倖幸",
+"倛丑" => "倛醜",
+"借助于" => "借助於",
"借着" => "借著",
+"借讬" => "借託",
+"倦游" => "倦遊",
+"假力于人" => "假力於人",
+"假药" => "假藥",
+"假讬" => "假託",
"假发" => "假髮",
+"偎干" => "偎乾",
+"偎干就湿" => "偎乾就濕",
+"偏后" => "偏後",
+"偏于" => "偏於",
+"做庄" => "做莊",
+"停停当当" => "停停當當",
+"停征" => "停徵",
"停制" => "停製",
"偷鸡不着" => "偷雞不著",
+"伪药" => "偽藥",
+"备注" => "備註",
"家伙" => "傢伙",
"家俱" => "傢俱",
"家具" => "傢具",
-"传布" => "傳佈",
-"债台高筑" => "債臺高築",
+"催并" => "催併",
+"佣人" => "傭人",
+"佣兵" => "傭兵",
+"佣工" => "傭工",
+"佣懒" => "傭懶",
+"佣书" => "傭書",
+"佣金" => "傭金",
+"伤痕累累" => "傷痕纍纍",
"傻里傻气" => "傻裡傻氣",
+"倾向于" => "傾向於",
"倾家荡产" => "傾家蕩產",
"倾复" => "傾複",
-"倾复" => "傾覆",
-"僱佣" => "僱佣",
+"仆人" => "僕人",
+"仆使" => "僕使",
+"仆仆" => "僕僕",
+"仆仆风尘" => "僕僕風塵",
+"仆僮" => "僕僮",
+"仆吏" => "僕吏",
+"仆固怀恩" => "僕固懷恩",
+"仆夫" => "僕夫",
+"仆姑" => "僕姑",
+"仆妇" => "僕婦",
+"仆射" => "僕射",
+"仆少" => "僕少",
+"仆役" => "僕役",
+"仆从" => "僕從",
+"仆憎" => "僕憎",
+"仆欧" => "僕歐",
+"仆程" => "僕程",
+"仆虽罢驽" => "僕雖罷駑",
+"侥幸" => "僥倖",
+"僮仆" => "僮僕",
+"雇主" => "僱主",
+"雇人" => "僱人",
+"雇佣" => "僱佣",
+"雇到" => "僱到",
+"雇员" => "僱員",
+"雇工" => "僱工",
+"雇用" => "僱用",
+"雇农" => "僱農",
+"仪范" => "儀範",
"仪表" => "儀錶",
+"亿多只" => "億多隻",
+"亿天后" => "億天後",
"亿只" => "億隻",
+"俭朴" => "儉樸",
+"儒略改革历" => "儒略改革曆",
+"儒略历" => "儒略曆",
"尽尽" => "儘儘",
"尽先" => "儘先",
"尽其所有" => "儘其所有",
@@ -2760,135 +3047,230 @@ $zh2Hant = array(
"尽是" => "儘是",
"尽管" => "儘管",
"尽速" => "儘速",
-"尽量" => "儘量",
-"允准" => "允准",
-"兄台" => "兄臺",
+"优于" => "優於",
+"优游" => "優遊",
+"兀术" => "兀朮",
+"元凶" => "元兇",
"充饥" => "充饑",
-"光采" => "光采",
-"克里" => "克裡",
+"凶器" => "兇器",
+"凶徒" => "兇徒",
+"凶手" => "兇手",
+"凶案" => "兇案",
+"凶残" => "兇殘",
+"凶杀" => "兇殺",
+"先占" => "先佔",
+"先后" => "先後",
+"先忧后乐" => "先憂後樂",
+"先采" => "先採",
+"先攻后守" => "先攻後守",
+"先于" => "先於",
+"先盛后衰" => "先盛後衰",
+"先礼后兵" => "先禮後兵",
+"先义后利" => "先義後利",
+"先苦后甘" => "先苦後甘",
+"先赢后输" => "先贏後輸",
+"先进后出" => "先進後出",
+"光采" => "光採",
+"光致致" => "光緻緻",
+"克药" => "克藥",
"克复" => "克複",
-"入伙" => "入伙",
+"免于" => "免於",
+"党参" => "党參",
+"党太尉" => "党太尉",
+"党进" => "党進",
+"党项" => "党項",
+"入夜后" => "入夜後",
+"入伙" => "入夥",
"内制" => "內製",
+"内斗" => "內鬥",
+"内哄" => "內鬨",
+"全干" => "全乾",
+"两天后" => "兩天後",
+"两扎" => "兩紮",
"两只" => "兩隻",
+"八天后" => "八天後",
"八字胡" => "八字鬍",
+"八扎" => "八紮",
"八只" => "八隻",
-"公布" => "公佈",
+"公仔面" => "公仔麵",
+"公仆" => "公僕",
"公干" => "公幹",
-"公斗" => "公斗",
"公历" => "公曆",
+"公诸于世" => "公諸於世",
+"公厘" => "公釐",
+"公余" => "公餘",
+"六划" => "六劃",
+"六天后" => "六天後",
+"六扎" => "六紮",
+"六冲" => "六衝",
"六只" => "六隻",
"六出" => "六齣",
+"其后" => "其後",
+"其次辟地" => "其次辟地",
+"其余" => "其餘",
+"典范" => "典範",
"兼并" => "兼併",
+"冉有仆" => "冉有僕",
+"再于" => "再於",
"冤雠" => "冤讎",
-"准予" => "准予",
-"准假" => "准假",
-"准将" => "准將",
-"准考证" => "准考證",
-"准许" => "准許",
+"冥蒙" => "冥濛",
+"冬山庄" => "冬山庄",
+"冬游" => "冬遊",
+"冶游" => "冶遊",
+"冷面相" => "冷面相",
+"冷面" => "冷麵",
+"凌蒙初" => "凌濛初",
+"凌藉" => "凌藉",
"几几" => "几几",
"几案" => "几案",
"几丝" => "几絲",
+"凡于" => "凡於",
"凹洞里" => "凹洞裡",
-"出征" => "出征",
+"出乖露丑" => "出乖露醜",
+"出于" => "出於",
+"出自于" => "出自於",
+"出谋划策" => "出謀劃策",
+"出游" => "出遊",
+"出丑" => "出醜",
"出锤" => "出鎚",
"刀削面" => "刀削麵",
-"刁斗" => "刁斗",
"分布" => "分佈",
-"切面" => "切麵",
-"刊布" => "刊佈",
-"划上" => "划上",
-"划下" => "划下",
-"划不来" => "划不來",
-"划了" => "划了",
-"划具" => "划具",
-"划出" => "划出",
-"划到" => "划到",
-"划动" => "划動",
-"划去" => "划去",
-"划子" => "划子",
-"划得来" => "划得來",
-"划拳" => "划拳",
-"划桨" => "划槳",
-"划水" => "划水",
-"划算" => "划算",
-"划船" => "划船",
-"划艇" => "划艇",
+"分占" => "分佔",
+"分钟" => "分鐘",
+"刑余" => "刑餘",
"划着" => "划著",
"划着走" => "划著走",
-"划行" => "划行",
-"划走" => "划走",
-"划起" => "划起",
-"划进" => "划進",
-"划过" => "划過",
-"初征" => "初征",
+"划龙舟" => "划龍舟",
+"别后" => "別後",
+"别日南鸿才北去" => "別日南鴻纔北去",
"别致" => "別緻",
"别着" => "別著",
+"别辟" => "別闢",
"别只" => "別隻",
-"利比里亚" => "利比裡亞",
+"利欲" => "利慾",
+"利于" => "利於",
"刮着" => "刮著",
+"刮风下雪倒便宜" => "刮風下雪倒便宜",
"刮胡刀" => "刮鬍刀",
+"制签" => "制籤",
+"刺绣" => "刺繡",
+"刻划" => "刻劃",
+"刻于" => "刻於",
+"刻钟" => "刻鐘",
"剃发" => "剃髮",
"剃须" => "剃鬚",
"削发" => "削髮",
+"削面" => "削麵",
"克制" => "剋制",
"克星" => "剋星",
-"克服" => "剋服",
"克死" => "剋死",
"克薄" => "剋薄",
-"前仆后继" => "前仆後繼",
-"前台" => "前臺",
-"前车之复" => "前車之覆",
-"刚才" => "剛纔",
+"前仰后合" => "前仰後合",
+"前倨后恭" => "前倨後恭",
+"前呼后拥" => "前呼後擁",
+"前后" => "前後",
+"前思后想" => "前思後想",
+"前挽后推" => "前挽後推",
+"前短后长" => "前短後長",
+"刚干" => "剛乾",
+"刚雇" => "剛僱",
+"刚才一载" => "剛纔一載",
+"剩余" => "剩餘",
+"剪牡丹喂牛" => "剪牡丹喂牛",
+"剪䌽" => "剪綵",
"剪发" => "剪髮",
"割舍" => "割捨",
"创制" => "創製",
-"加里宁" => "加裡寧",
+"划一" => "劃一",
+"划上" => "劃上",
+"划下" => "劃下",
+"划了" => "劃了",
+"划出" => "劃出",
+"划分" => "劃分",
+"划到" => "劃到",
+"划划" => "劃劃",
+"划去" => "劃去",
+"划在" => "劃在",
+"划地" => "劃地",
+"划定" => "劃定",
+"划得" => "劃得",
+"划成" => "劃成",
+"划掉" => "劃掉",
+"划拨" => "劃撥",
+"划时代" => "劃時代",
+"划款" => "劃款",
+"划归" => "劃歸",
+"划法" => "劃法",
+"划清" => "劃清",
+"划界" => "劃界",
+"划破" => "劃破",
+"划线" => "劃線",
+"划足" => "劃足",
+"划开" => "劃開",
+"力争上游" => "力爭上遊",
+"功致" => "功緻",
+"加害于" => "加害於",
+"加工余量" => "加工餘量",
+"加卷" => "加捲",
+"加于" => "加於",
+"加药" => "加藥",
+"加注" => "加註",
+"劣于" => "劣於",
+"助于" => "助於",
+"劫余" => "劫餘",
+"勃郁" => "勃鬱",
+"勇于" => "勇於",
"动荡" => "動蕩",
+"胜于" => "勝於",
"劳力士表" => "勞力士錶",
-"包准" => "包准",
+"勤仆" => "勤僕",
+"勤朴" => "勤樸",
+"勾干" => "勾幹",
+"勾心斗角" => "勾心鬥角",
+"勿施于人" => "勿施於人",
"包谷" => "包穀",
-"北斗" => "北斗",
-"北回" => "北迴",
+"包扎" => "包紮",
+"北岳" => "北嶽",
+"北回线" => "北迴線",
+"北回铁路" => "北迴鐵路",
"匡复" => "匡複",
"匪干" => "匪幹",
-"十卷" => "十卷",
-"十台" => "十臺",
+"匿于" => "匿於",
+"区划" => "區劃",
+"十划" => "十劃",
+"十多只" => "十多隻",
+"十天后" => "十天後",
+"十卷" => "十捲",
+"十扎" => "十紮",
"十只" => "十隻",
"十出" => "十齣",
+"千多只" => "千多隻",
+"千天后" => "千天後",
+"千扎" => "千紮",
"千丝万缕" => "千絲萬縷",
"千回百折" => "千迴百折",
"千回百转" => "千迴百轉",
"千钧一发" => "千鈞一髮",
"千只" => "千隻",
-"升斗小民" => "升斗小民",
+"午后" => "午後",
+"半于" => "半於",
"半只" => "半隻",
"南岳" => "南嶽",
-"南征" => "南征",
-"南台" => "南臺",
-"南回" => "南迴",
-"卡里" => "卡裡",
+"南筑" => "南筑",
+"南回线" => "南迴線",
+"南回铁路" => "南迴鐵路",
+"南游" => "南遊",
+"博汇" => "博彙",
+"博采" => "博採",
+"印累绶若" => "印纍綬若",
"印制" => "印製",
-"卷入" => "卷入",
-"卷取" => "卷取",
-"卷土重来" => "卷土重來",
-"卷子" => "卷子",
-"卷宗" => "卷宗",
-"卷尺" => "卷尺",
-"卷层云" => "卷層雲",
-"卷帙" => "卷帙",
-"卷扬机" => "卷揚機",
-"卷曲" => "卷曲",
-"卷染" => "卷染",
-"卷烟" => "卷煙",
-"卷筒" => "卷筒",
-"卷纬" => "卷緯",
-"卷绕" => "卷繞",
-"卷装" => "卷裝",
-"卷轴" => "卷軸",
-"卷云" => "卷雲",
-"卷领" => "卷領",
+"危于" => "危於",
"卷发" => "卷髮",
"卷须" => "卷鬚",
+"厂部" => "厂部",
+"原子钟" => "原子鐘",
+"原于" => "原於",
+"历物之意" => "厤物之意",
"参与" => "參与",
"参与者" => "參与者",
"参合" => "參合",
@@ -2901,375 +3283,921 @@ $zh2Hant = array(
"参观团" => "參觀團",
"参观团体" => "參觀團體",
"参阅" => "參閱",
+"及于" => "及於",
+"反于" => "反於",
+"反朴" => "反樸",
"反冲" => "反衝",
-"反复" => "反複",
+"反复制" => "反複製",
"反复" => "反覆",
+"取信于" => "取信於",
"取舍" => "取捨",
+"取材于" => "取材於",
+"取决于" => "取決於",
+"取法于" => "取法於",
+"受人之讬" => "受人之託",
+"受制于人" => "受制於人",
+"受讬" => "受託",
+"受阻于" => "受阻於",
+"口干" => "口乾",
+"口燥脣干" => "口燥脣乾",
+"口腹之欲" => "口腹之慾",
+"口血未干" => "口血未乾",
"口里" => "口裡",
-"只准" => "只准",
+"古柯硷" => "古柯鹼",
+"古朴" => "古樸",
+"另于" => "另於",
+"另辟" => "另闢",
+"叩钟" => "叩鐘",
+"只占" => "只佔",
+"只采" => "只採",
"只冲" => "只衝",
"叮当" => "叮噹",
-"可怜虫" => "可憐虫",
+"可于" => "可於",
"可紧可松" => "可緊可鬆",
+"台后" => "台後",
+"台历" => "台曆",
"台制" => "台製",
-"司令台" => "司令臺",
+"右后" => "右後",
+"叶恭弘" => "叶恭弘",
+"叶 恭弘" => "叶 恭弘",
+"叶 恭弘" => "叶 恭弘",
+"叶音" => "叶音",
+"叶韵" => "叶韻",
+"吃板刀面" => "吃板刀麵",
"吃着不尽" => "吃著不盡",
+"吃姜" => "吃薑",
+"吃药" => "吃藥",
"吃里扒外" => "吃裡扒外",
"吃里爬外" => "吃裡爬外",
-"各吊" => "各吊",
-"合伙" => "合伙",
+"吃豆干" => "吃豆乾",
+"吃辣面" => "吃辣麵",
+"吃错药" => "吃錯藥",
+"各辟" => "各闢",
"合并" => "合併",
+"合伙" => "合夥",
+"合采" => "合採",
+"合于" => "合於",
+"合历" => "合曆",
"合着" => "合著",
"合着者" => "合著者",
-"吊上" => "吊上",
-"吊下" => "吊下",
-"吊了" => "吊了",
-"吊个" => "吊個",
-"吊儿郎当" => "吊兒郎當",
-"吊到" => "吊到",
-"吊去" => "吊去",
-"吊取" => "吊取",
-"吊吊" => "吊吊",
-"吊嗓" => "吊嗓",
-"吊好" => "吊好",
-"吊子" => "吊子",
-"吊带" => "吊帶",
"吊带裤" => "吊帶褲",
-"吊床" => "吊床",
-"吊得" => "吊得",
-"吊挂" => "吊掛",
"吊挂着" => "吊掛著",
-"吊杆" => "吊杆",
-"吊架" => "吊架",
-"吊桶" => "吊桶",
-"吊杆" => "吊桿",
-"吊桥" => "吊橋",
-"吊死" => "吊死",
-"吊灯" => "吊燈",
-"吊环" => "吊環",
-"吊盘" => "吊盤",
-"吊索" => "吊索",
"吊着" => "吊著",
-"吊装" => "吊裝",
"吊裤" => "吊褲",
"吊裤带" => "吊褲帶",
-"吊袜" => "吊襪",
-"吊走" => "吊走",
-"吊起" => "吊起",
-"吊车" => "吊車",
-"吊钩" => "吊鉤",
-"吊销" => "吊銷",
"吊钟" => "吊鐘",
-"同伙" => "同伙",
-"名表" => "名錶",
-"后冠" => "后冠",
-"后土" => "后土",
-"后妃" => "后妃",
-"后座" => "后座",
-"后稷" => "后稷",
-"后羿" => "后羿",
-"后里" => "后里",
+"同伙" => "同夥",
+"同于" => "同於",
+"名闻于世" => "名聞於世",
+"后发座" => "后髮座",
+"向后" => "向後",
"向着" => "向著",
"吞并" => "吞併",
+"吟游" => "吟遊",
+"吹干" => "吹乾",
"吹发" => "吹髮",
-"吕后" => "呂后",
-"獃里獃气" => "呆裡呆氣",
-"周而复始" => "周而複始",
+"呆致致" => "呆緻緻",
+"呆里呆气" => "呆裡呆氣",
+"周历" => "周曆",
+"周杰伦" => "周杰倫",
+"周游" => "周遊",
"呼吁" => "呼籲",
-"和面" => "和麵",
+"咬姜呷醋" => "咬薑呷醋",
+"咯当" => "咯噹",
+"咳嗽药" => "咳嗽藥",
+"哀吊" => "哀弔",
+"品汇" => "品彙",
+"员山庄" => "員山庄",
"哪里" => "哪裡",
"哭脏" => "哭髒",
-"问卷" => "問卷",
-"喝采" => "喝采",
-"单干" => "單干",
+"唇干" => "唇乾",
+"唱游" => "唱遊",
+"唾面自干" => "唾面自乾",
+"唾余" => "唾餘",
+"商历" => "商曆",
+"问政于民" => "問政於民",
+"问道于盲" => "問道於盲",
+"啷当" => "啷噹",
+"善后" => "善後",
+"善于" => "善於",
+"喜形于色" => "喜形於色",
+"喧哄" => "喧鬨",
+"丧钟" => "喪鐘",
+"单干" => "單幹",
+"单打独斗" => "單打獨鬥",
"单只" => "單隻",
+"嗑药" => "嗑藥",
+"嗣后" => "嗣後",
+"嘉谷" => "嘉穀",
"嘴里" => "嘴裡",
"恶心" => "噁心",
+"噙齿戴发" => "噙齒戴髮",
"当啷" => "噹啷",
"当当" => "噹噹",
"噜苏" => "嚕囌",
"向导" => "嚮導",
"向往" => "嚮往",
"向应" => "嚮應",
-"向日" => "嚮日",
"向迩" => "嚮邇",
+"严于" => "嚴於",
"严丝合缝" => "嚴絲合縫",
-"严复" => "嚴複",
+"嚼谷" => "嚼穀",
+"囉囉苏苏" => "囉囉囌囌",
+"囉苏" => "囉囌",
+"嘱讬" => "囑託",
+"四分历" => "四分曆",
+"四天后" => "四天後",
"四舍五入" => "四捨五入",
+"四扎" => "四紮",
"四只" => "四隻",
"四出" => "四齣",
+"回采" => "回採",
+"回历" => "回曆",
"回丝" => "回絲",
"回着" => "回著",
"回荡" => "回蕩",
-"回复" => "回覆",
-"回采" => "回采",
+"回游" => "回遊",
+"因于" => "因於",
+"困于" => "困於",
+"困兽之斗" => "困獸之鬥",
+"困兽犹斗" => "困獸猶鬥",
+"固于" => "固於",
+"囿于" => "囿於",
+"囿于一时" => "囿於一時",
+"囿于成见" => "囿於成見",
"圈子里" => "圈子裡",
+"圈梁" => "圈樑",
"圈里" => "圈裡",
+"国之桢干" => "國之楨榦",
+"国于" => "國於",
"国历" => "國曆",
+"国历代" => "國歷代",
+"国历史" => "國歷史",
"国雠" => "國讎",
"园里" => "園裡",
+"园游会" => "園遊會",
"图里" => "圖裡",
"土里" => "土裡",
"土制" => "土製",
+"在于" => "在於",
"地志" => "地誌",
-"坍台" => "坍臺",
+"地丑德齐" => "地醜德齊",
+"坏于" => "坏於",
+"坐钟" => "坐鐘",
"坑里" => "坑裡",
+"坤范" => "坤範",
"坦荡" => "坦蕩",
+"坱郁" => "坱鬱",
+"垂直于" => "垂直於",
"垂发" => "垂髮",
-"垮台" => "垮臺",
-"埋布" => "埋佈",
+"型范" => "型範",
+"埃及历" => "埃及曆",
"城里" => "城裡",
"基干" => "基幹",
-"报复" => "報複",
-"塌台" => "塌臺",
-"塔台" => "塔臺",
+"基于" => "基於",
+"基准" => "基準",
+"坚致" => "堅緻",
"涂着" => "塗著",
+"涂药" => "塗藥",
+"塞耳盗钟" => "塞耳盜鐘",
+"塞药" => "塞藥",
"墓志" => "墓誌",
-"墨斗" => "墨斗",
-"墨索里尼" => "墨索裡尼",
+"增辟" => "增闢",
+"墨沈" => "墨沈",
+"堕胎药" => "墮胎藥",
"垦复" => "墾複",
+"垦辟" => "墾闢",
"垄断价格" => "壟斷價格",
"垄断资产" => "壟斷資產",
"垄断集团" => "壟斷集團",
+"壮面" => "壯麵",
+"壹郁" => "壹鬱",
"壶里" => "壺裡",
+"壸范" => "壼範",
"寿面" => "壽麵",
"夏天里" => "夏天裡",
"夏历" => "夏曆",
+"夏历史" => "夏歷史",
+"夏游" => "夏遊",
+"外强中干" => "外強中乾",
"外制" => "外製",
+"多划" => "多劃",
+"多天后" => "多天後",
+"多于" => "多於",
"多冲" => "多衝",
-"多采多姿" => "多采多姿",
+"多丑" => "多醜",
+"多只" => "多隻",
+"多余" => "多餘",
"多么" => "多麼",
"夜光表" => "夜光錶",
"夜里" => "夜裡",
+"夜游" => "夜遊",
"梦里" => "夢裡",
-"大伙" => "大伙",
-"大卷" => "大卷",
-"大干" => "大干",
+"梦游" => "夢遊",
+"伙伴" => "夥伴",
+"伙友" => "夥友",
+"伙同" => "夥同",
+"伙众" => "夥眾",
+"伙计" => "夥計",
+"大伙" => "大夥",
"大干" => "大幹",
+"大批涌到" => "大批湧到",
+"大于" => "大於",
+"大明历" => "大明曆",
+"大历" => "大曆",
+"大梁" => "大樑",
+"大目干连" => "大目乾連",
+"大衍历" => "大衍曆",
+"大言非夸" => "大言非夸",
+"大丑" => "大醜",
"大锤" => "大鎚",
"大只" => "大隻",
-"天后" => "天后",
-"天干" => "天干",
-"天文台" => "天文臺",
+"天干物燥" => "天乾物燥",
+"天克地冲" => "天克地衝",
+"天后" => "天後",
+"0天后" => "0天後",
+"天文钟" => "天文鐘",
+"天然硷" => "天然鹼",
"天翻地复" => "天翻地覆",
-"太后" => "太后",
+"天复地载" => "天覆地載",
+"太仆" => "太僕",
+"太初历" => "太初曆",
+"夯干" => "夯幹",
+"失信于人" => "失信於人",
+"失于" => "失於",
+"夸人" => "夸人",
+"夸克" => "夸克",
+"夸姣" => "夸姣",
+"夸容" => "夸容",
+"夸毗" => "夸毗",
+"夸父" => "夸父",
+"夸特" => "夸特",
+"夸丽" => "夸麗",
+"奇丑" => "奇醜",
"奏折" => "奏摺",
-"女丑" => "女丑",
-"女佣" => "女佣",
-"好家夥" => "好傢夥",
-"好戏连台" => "好戲連臺",
+"奏于" => "奏於",
+"夺斗" => "奪鬥",
+"奋斗" => "奮鬥",
+"女佣" => "女傭",
+"女仆" => "女僕",
+"奴仆" => "奴僕",
+"好干" => "好乾",
+"好家伙" => "好傢夥",
+"好于" => "好於",
+"好签" => "好籤",
+"好丑" => "好醜",
+"如于" => "如於",
+"如果干" => "如果幹",
"如法泡制" => "如法泡製",
-"妆台" => "妝臺",
-"姜太公" => "姜太公",
-"姜子牙" => "姜子牙",
+"妙药" => "妙藥",
+"始于" => "始於",
+"委罪于人" => "委罪於人",
+"委讬" => "委託",
"姜丝" => "姜絲",
+"奸夫" => "姦夫",
+"奸妇" => "姦婦",
+"奸情" => "姦情",
+"奸杀" => "姦殺",
+"奸污" => "姦汙",
+"奸淫" => "姦淫",
+"奸邪" => "姦邪",
+"威棱" => "威稜",
+"婚后" => "婚後",
+"婢仆" => "婢僕",
+"嫁于" => "嫁於",
+"嫁祸于人" => "嫁禍於人",
+"嫌好道丑" => "嫌好道醜",
+"娴于" => "嫻於",
+"嬉游" => "嬉遊",
+"嬴余" => "嬴餘",
+"子之丰兮" => "子之丰兮",
"字汇" => "字彙",
"字里行间" => "字裡行間",
+"存十一于千百" => "存十一於千百",
"存折" => "存摺",
-"孟姜女" => "孟姜女",
+"季后赛" => "季後賽",
+"孤寡不谷" => "孤寡不穀",
"宇宙志" => "宇宙誌",
-"定准" => "定准",
+"安于" => "安於",
+"安沈铁路" => "安瀋鐵路",
+"安眠药" => "安眠藥",
+"安胎药" => "安胎藥",
+"完工后" => "完工後",
+"完成后" => "完成後",
+"宗周钟" => "宗周鐘",
+"官地为采" => "官地為寀",
+"官历" => "官曆",
+"官庄" => "官莊",
+"定于" => "定於",
+"定准" => "定準",
"定制" => "定製",
-"宣布" => "宣佈",
+"定都于" => "定都於",
+"宜于" => "宜於",
+"宦游" => "宦遊",
"宫里" => "宮裡",
-"家伙" => "家伙",
+"害于" => "害於",
+"宴游" => "宴遊",
+"家仆" => "家僕",
+"家庄" => "家莊",
"家里" => "家裡",
-"密布" => "密佈",
+"家丑" => "家醜",
+"容后说明" => "容後說明",
+"容于" => "容於",
+"容范" => "容範",
+"寄于" => "寄於",
+"寄讬" => "寄託",
"寇雠" => "寇讎",
+"富于" => "富於",
+"富余" => "富餘",
+"寒栗" => "寒慄",
+"寒于" => "寒於",
+"寓兵于农" => "寓兵於農",
+"寓教于乐" => "寓教於樂",
+"寓于" => "寓於",
+"寡欲" => "寡慾",
"实干" => "實幹",
"写字台" => "寫字檯",
-"写字台" => "寫字臺",
+"宽于" => "寬於",
+"宽余" => "寬餘",
"宽松" => "寬鬆",
+"寮采" => "寮寀",
+"宝山庄" => "寶山庄",
+"宝历" => "寶曆",
"封面里" => "封面裡",
-"射干" => "射干",
+"射雕" => "射鵰",
+"将于" => "將於",
+"专美于前" => "專美於前",
+"专注" => "專註",
+"对折" => "對摺",
+"对于" => "對於",
+"对准" => "對準",
+"对华发动" => "對華發動",
"对表" => "對錶",
-"小丑" => "小丑",
-"小伙" => "小伙",
+"导游" => "導遊",
+"小仆" => "小僕",
+"小伙子" => "小夥子",
+"小于" => "小於",
+"小米面" => "小米麵",
"小只" => "小隻",
-"少吊" => "少吊",
-"尺布斗粟" => "尺布斗粟",
+"少采" => "少採",
+"少于" => "少於",
+"就于" => "就於",
+"就范" => "就範",
+"就读于" => "就讀於",
+"尸魂界" => "尸魂界",
"尼克松" => "尼克鬆",
-"尼采" => "尼采",
-"尿斗" => "尿斗",
"局里" => "局裡",
-"居里" => "居裡",
+"居于" => "居於",
+"屈服于" => "屈服於",
"屋子里" => "屋子裡",
+"屋梁" => "屋樑",
"屋里" => "屋裡",
-"展布" => "展佈",
-"屡仆屡起" => "屢仆屢起",
+"屑于" => "屑於",
+"屡顾尔仆" => "屢顧爾僕",
+"属意于" => "屬意於",
+"属于" => "屬於",
+"屯扎" => "屯紮",
"屯里" => "屯裡",
+"山崩钟应" => "山崩鐘應",
"山岳" => "山嶽",
+"山后" => "山後",
+"山梁" => "山樑",
+"山庄" => "山莊",
+"山药" => "山藥",
"山里" => "山裡",
"峰回" => "峰迴",
+"昆剧" => "崑劇",
+"昆山" => "崑山",
+"昆仑" => "崑崙",
+"昆曲" => "崑曲",
+"昆腔" => "崑腔",
+"昆苏" => "崑蘇",
+"昆调" => "崑調",
+"仑背" => "崙背",
+"嶒棱" => "嶒稜",
+"岳麓山" => "嶽麓山",
+"川谷" => "川穀",
"巡回" => "巡迴",
+"巡游" => "巡遊",
+"工于" => "工於",
+"工致" => "工緻",
+"左后" => "左後",
+"左冲右突" => "左衝右突",
+"巧妇做不得无面馎饦" => "巧婦做不得無麵餺飥",
"巧干" => "巧幹",
+"巧历" => "巧曆",
+"差于" => "差於",
+"已于" => "已於",
"巴尔干" => "巴爾幹",
-"巴里" => "巴裡",
"巷里" => "巷裡",
"市里" => "市裡",
"布谷" => "布穀",
-"希腊" => "希腊",
+"希伯来历" => "希伯來曆",
"帘子" => "帘子",
"帘布" => "帘布",
-"席卷" => "席卷",
+"师范" => "師範",
+"席卷" => "席捲",
"带团参加" => "帶團參加",
"带发修行" => "帶髮修行",
-"干休" => "干休",
+"幕后" => "幕後",
+"帮佣" => "幫傭",
"干系" => "干係",
-"干卿何事" => "干卿何事",
-"干将" => "干將",
-"干戈" => "干戈",
-"干挠" => "干撓",
-"干扰" => "干擾",
-"干支" => "干支",
-"干政" => "干政",
-"干时" => "干時",
-"干涉" => "干涉",
-"干犯" => "干犯",
-"干与" => "干與",
"干着急" => "干著急",
-"干贝" => "干貝",
-"干预" => "干預",
-"平台" => "平臺",
+"平平当当" => "平平當當",
+"平准" => "平準",
+"年后" => "年後",
"年历" => "年曆",
+"年历史" => "年歷史",
+"年谷" => "年穀",
"年里" => "年裡",
+"并州" => "并州",
"干上" => "幹上",
"干下去" => "幹下去",
"干了" => "幹了",
"干事" => "幹事",
"干些" => "幹些",
+"干人" => "幹人",
+"干什么" => "幹什麼",
"干个" => "幹個",
"干劲" => "幹勁",
+"干吏" => "幹吏",
"干员" => "幹員",
"干吗" => "幹嗎",
"干嘛" => "幹嘛",
"干坏事" => "幹壞事",
"干完" => "幹完",
+"干家" => "幹家",
"干得" => "幹得",
"干性油" => "幹性油",
"干才" => "幹才",
"干掉" => "幹掉",
+"干探" => "幹探",
"干校" => "幹校",
"干活" => "幹活",
"干流" => "幹流",
+"干济" => "幹濟",
+"干营生" => "幹營生",
+"干父之蛊" => "幹父之蠱",
"干球温度" => "幹球溫度",
+"干当" => "幹當",
+"干的停当" => "幹的停當",
+"干细胞" => "幹細胞",
"干线" => "幹線",
"干练" => "幹練",
+"干缺" => "幹缺",
+"干蛊" => "幹蠱",
"干警" => "幹警",
"干起来" => "幹起來",
"干路" => "幹路",
"干道" => "幹道",
"干部" => "幹部",
+"干革命" => "幹革命",
+"干头" => "幹頭",
"干么" => "幹麼",
+"几划" => "幾劃",
+"几天后" => "幾天後",
+"几于" => "幾於",
"几丝" => "幾絲",
"几只" => "幾隻",
"几出" => "幾齣",
-"底里" => "底裡",
-"康采恩" => "康采恩",
+"广部" => "广部",
+"府干卿" => "府干卿",
+"府干扰" => "府干擾",
+"府干政" => "府干政",
+"府干涉" => "府干涉",
+"府干犯" => "府干犯",
+"府干预" => "府干預",
+"府干" => "府幹",
+"府后" => "府後",
+"座钟" => "座鐘",
+"康采恩" => "康採恩",
+"康庄" => "康莊",
+"厨余" => "廚餘",
"庙里" => "廟裡",
-"建台" => "建臺",
+"广舍" => "廣捨",
+"延后" => "延後",
+"建于" => "建於",
+"建都于" => "建都於",
+"弄干" => "弄乾",
+"弄丑" => "弄醜",
"弄脏" => "弄髒",
-"弔卷" => "弔卷",
+"弄松" => "弄鬆",
+"吊儿郎当" => "弔兒郎當",
+"吊卷" => "弔卷",
+"吊古" => "弔古",
+"吊唁" => "弔唁",
+"吊丧" => "弔喪",
+"吊孝" => "弔孝",
+"吊客" => "弔客",
+"吊带" => "弔帶",
+"吊慰" => "弔慰",
+"吊挂" => "弔掛",
+"吊文" => "弔文",
+"吊死" => "弔死",
+"吊民伐罪" => "弔民伐罪",
+"吊祭" => "弔祭",
"弘历" => "弘曆",
+"弱于" => "弱於",
+"弱硷" => "弱鹼",
+"张三丰" => "張三丰",
+"强占" => "強佔",
+"强奸" => "強姦",
+"强干" => "強幹",
+"强于" => "強於",
+"强硷" => "強鹼",
+"别口气" => "彆口氣",
+"别强" => "彆強",
"别扭" => "彆扭",
"别拗" => "彆拗",
"别气" => "彆氣",
-"别脚" => "彆腳",
"别着" => "彆著",
"弹子台" => "彈子檯",
-"弹药" => "彈葯",
+"弹药" => "彈藥",
"汇报" => "彙報",
"汇整" => "彙整",
"汇编" => "彙編",
-"汇总" => "彙總",
"汇纂" => "彙纂",
"汇辑" => "彙輯",
"汇集" => "彙集",
"形单影只" => "形單影隻",
-"影后" => "影后",
+"形于" => "形於",
+"役于" => "役於",
+"役于外物" => "役於外物",
+"往来于" => "往來於",
+"往后" => "往後",
"往里" => "往裡",
"往复" => "往複",
-"征伐" => "征伐",
-"征兵" => "征兵",
-"征尘" => "征塵",
-"征夫" => "征夫",
-"征战" => "征戰",
-"征收" => "征收",
-"征服" => "征服",
-"征求" => "征求",
-"征发" => "征發",
-"征衣" => "征衣",
-"征讨" => "征討",
-"征途" => "征途",
-"后台" => "後臺",
+"很干" => "很乾",
+"律历志" => "律曆志",
+"后上" => "後上",
+"后下" => "後下",
+"后世" => "後世",
+"后主" => "後主",
+"后事" => "後事",
+"后人" => "後人",
+"后代" => "後代",
+"后仰" => "後仰",
+"后件" => "後件",
+"后任" => "後任",
+"后作" => "後作",
+"后来" => "後來",
+"后偏" => "後偏",
+"后备" => "後備",
+"后传" => "後傳",
+"后分" => "後分",
+"后到" => "後到",
+"后力不继" => "後力不繼",
+"后劲" => "後勁",
+"后勤" => "後勤",
+"后区" => "後區",
+"后半" => "後半",
+"后印" => "後印",
+"后去" => "後去",
+"后台" => "後台",
+"后向" => "後向",
+"后周" => "後周",
+"后唐" => "後唐",
+"后嗣" => "後嗣",
+"后园" => "後園",
+"后图" => "後圖",
+"后土" => "後土",
+"后埔" => "後埔",
+"后堂" => "後堂",
+"后尘" => "後塵",
+"后壁" => "後壁",
+"后天" => "後天",
+"后奏" => "後奏",
+"后娘" => "後娘",
+"后学" => "後學",
+"后宫" => "後宮",
+"后山" => "後山",
+"后巷" => "後巷",
+"后市" => "後市",
+"后年" => "後年",
+"后几" => "後幾",
+"后庄" => "後庄",
+"后序" => "後序",
+"后座" => "後座",
+"后悔" => "後悔",
+"后患" => "後患",
+"后房" => "後房",
+"后手" => "後手",
+"后排" => "後排",
+"后掠角" => "後掠角",
+"后接" => "後接",
+"后援" => "後援",
+"后撤" => "後撤",
+"后攻" => "後攻",
+"后放" => "後放",
+"后效" => "後效",
+"后文" => "後文",
+"后方" => "後方",
+"后于" => "後於",
+"后日" => "後日",
+"后晋" => "後晉",
+"后晌" => "後晌",
+"后晚" => "後晚",
+"后景" => "後景",
+"后会" => "後會",
+"后有" => "後有",
+"后望镜" => "後望鏡",
+"后期" => "後期",
+"后果" => "後果",
+"后桅" => "後桅",
+"后梁" => "後梁",
+"后桥" => "後橋",
+"后步" => "後步",
+"后段" => "後段",
+"后殿" => "後殿",
+"后母" => "後母",
+"后派" => "後派",
+"后浪" => "後浪",
+"后凉" => "後涼",
+"后港" => "後港",
+"后汉" => "後漢",
+"后为" => "後為",
+"后无来者" => "後無來者",
+"后燕" => "後燕",
+"后生" => "後生",
+"后用" => "後用",
+"后由" => "後由",
+"后盾" => "後盾",
+"后知" => "後知",
+"后福" => "後福",
+"后秃" => "後禿",
+"后秦" => "後秦",
+"后空翻" => "後空翻",
+"后窗" => "後窗",
+"后站" => "後站",
+"后端" => "後端",
+"后竹围" => "後竹圍",
+"后节" => "後節",
+"后篇" => "後篇",
+"后继" => "後繼",
+"后续" => "後續",
+"后置" => "後置",
+"后者" => "後者",
+"后肢" => "後肢",
+"后背" => "後背",
+"后脑" => "後腦",
+"后脚" => "後腳",
+"后腿" => "後腿",
+"后膛" => "後膛",
+"后花园" => "後花園",
+"后菜园" => "後菜園",
+"后叶" => "後葉",
+"后行" => "後行",
+"后街" => "後街",
+"后卫" => "後衛",
+"后裔" => "後裔",
+"后䙓" => "後襬",
+"后视镜" => "後視鏡",
+"后计" => "後計",
+"后记" => "後記",
+"后设" => "後設",
+"后读" => "後讀",
+"后走" => "後走",
+"后起" => "後起",
+"后赵" => "後趙",
+"后足" => "後足",
+"后跟" => "後跟",
+"后路" => "後路",
+"后身" => "後身",
+"后车" => "後車",
+"后辈" => "後輩",
+"后轮" => "後輪",
+"后转" => "後轉",
+"后述" => "後述",
+"后退" => "後退",
+"后送" => "後送",
+"后进" => "後進",
+"后过" => "後過",
+"后遗症" => "後遺症",
+"后边" => "後邊",
+"后部" => "後部",
+"后镜" => "後鏡",
+"后门" => "後門",
+"后防" => "後防",
+"后院" => "後院",
+"后集" => "後集",
+"后面" => "後面",
+"后项" => "後項",
+"后头" => "後頭",
+"后颈" => "後頸",
+"后顾" => "後顧",
+"后魏" => "後魏",
+"后点" => "後點",
+"后龙" => "後龍",
+"徐干" => "徐幹",
+"徒讬空言" => "徒託空言",
+"得于" => "得於",
+"徜徉于" => "徜徉於",
+"从事于" => "從事於",
+"从于" => "從於",
"从里到外" => "從裡到外",
"从里向外" => "從裡向外",
+"复始" => "復始",
"复雠" => "復讎",
-"复辟" => "復辟",
-"德干高原" => "德干高原",
+"征信" => "徵信",
+"征候" => "徵候",
+"征兆" => "徵兆",
+"征兵" => "徵兵",
+"征到" => "徵到",
+"征募" => "徵募",
+"征友" => "徵友",
+"征召" => "徵召",
+"征引" => "徵引",
+"征得" => "徵得",
+"征收" => "徵收",
+"征文" => "徵文",
+"征求" => "徵求",
+"征状" => "徵狀",
+"征用" => "徵用",
+"征税" => "徵稅",
+"征稿" => "徵稿",
+"征结" => "徵結",
+"征聘" => "徵聘",
+"征训" => "徵訓",
+"征询" => "徵詢",
+"征调" => "徵調",
+"征象" => "徵象",
+"征购" => "徵購",
+"征集" => "徵集",
+"征验出" => "徵驗出",
"心愿" => "心愿",
+"心于" => "心於",
+"心细如发" => "心細如髮",
"心荡神驰" => "心蕩神馳",
+"心药" => "心藥",
"心里" => "心裡",
+"心余" => "心餘",
+"志于" => "志於",
+"忙并" => "忙併",
+"忙于" => "忙於",
"忙里" => "忙裡",
+"忠仆" => "忠僕",
+"忠于" => "忠於",
+"快干" => "快乾",
"快干" => "快幹",
+"快快当当" => "快快當當",
"快冲" => "快衝",
+"忽前忽后" => "忽前忽後",
"怎么" => "怎麼",
"怎么着" => "怎麼著",
+"怒形于色" => "怒形於色",
+"怒于" => "怒於",
"怒发冲冠" => "怒髮衝冠",
+"思前思后" => "思前思後",
+"思前想后" => "思前想後",
+"急于" => "急於",
"急冲而下" => "急衝而下",
+"性征" => "性徵",
+"性欲" => "性慾",
"怪里怪气" => "怪裡怪氣",
-"恩准" => "恩准",
-"情有所钟" => "情有所鍾",
+"怫郁" => "怫鬱",
+"息谷" => "息穀",
+"恰才" => "恰纔",
+"悍药" => "悍藥",
+"悒郁" => "悒鬱",
+"悒郁寡欢" => "悒鬱寡歡",
+"悠游" => "悠遊",
+"闷着头儿干" => "悶著頭兒幹",
+"悸栗" => "悸慄",
+"情欲" => "情慾",
+"惇朴" => "惇樸",
+"恶直丑正" => "惡直醜正",
+"惴栗" => "惴慄",
+"意大利面" => "意大利麵",
"意面" => "意麵",
+"爱困" => "愛睏",
+"感冒药" => "感冒藥",
+"感于" => "感於",
+"愧于" => "愧於",
+"愿朴" => "愿樸",
+"愿而恭" => "愿而恭",
"慌里慌张" => "慌裡慌張",
-"慰借" => "慰藉",
-"忧郁" => "憂郁",
-"凭吊" => "憑吊",
-"凭借" => "憑藉",
-"凭借着" => "憑藉著",
+"惯于" => "慣於",
+"慰藉" => "慰藉",
+"庆吊" => "慶弔",
+"庆历" => "慶曆",
+"欲令智昏" => "慾令智昏",
+"欲壑难填" => "慾壑難填",
+"欲念" => "慾念",
+"欲望" => "慾望",
+"欲海" => "慾海",
+"欲火" => "慾火",
+"欲障" => "慾障",
+"忧形于色" => "憂形於色",
+"忧郁" => "憂鬱",
+"凭吊" => "憑弔",
+"凭藉着" => "憑藉著",
+"恳讬" => "懇託",
+"懈松" => "懈鬆",
+"应征" => "應徵",
+"应钟" => "應鐘",
"蒙懂" => "懞懂",
+"蒙蒙懂懂" => "懞懞懂懂",
+"蒙直" => "懞直",
+"惩前毖后" => "懲前毖後",
+"懒于" => "懶於",
"怀里" => "懷裡",
"怀表" => "懷錶",
-"悬吊" => "懸吊",
+"悬梁" => "懸樑",
+"悬臂梁" => "懸臂樑",
+"悬钟" => "懸鐘",
+"惧于" => "懼於",
+"懿范" => "懿範",
"恋恋不舍" => "戀戀不捨",
-"戏台" => "戲臺",
+"成于" => "成於",
+"成药" => "成藥",
+"或于" => "或於",
+"戬谷" => "戩穀",
+"截发" => "截髮",
+"战天斗地" => "戰天鬥地",
+"战后" => "戰後",
+"战栗" => "戰慄",
+"战斗" => "戰鬥",
"戴表" => "戴錶",
-"戽斗" => "戽斗",
"房里" => "房裡",
-"手不释卷" => "手不釋卷",
-"手卷" => "手卷",
+"扁拟谷盗虫" => "扁擬穀盜蟲",
+"手冢治虫" => "手塚治虫",
"手折" => "手摺",
"手里" => "手裡",
"手表" => "手錶",
"手松" => "手鬆",
"才干" => "才幹",
-"才高八斗" => "才高八斗",
+"打干哕" => "打乾噦",
+"打并" => "打併",
+"打卡钟" => "打卡鐘",
+"打干" => "打幹",
+"打拼" => "打拚",
"打谷" => "打穀",
+"打钟" => "打鐘",
+"打斗" => "打鬥",
"扞御" => "扞禦",
-"批准" => "批准",
+"扯面" => "扯麵",
+"批准的" => "批准的",
"批复" => "批複",
-"批复" => "批覆",
+"批注" => "批註",
+"批斗" => "批鬥",
+"承先启后" => "承先啟後",
"承制" => "承製",
+"抑郁" => "抑鬱",
+"抓奸" => "抓姦",
+"抓药" => "抓藥",
+"抓斗" => "抓鬥",
+"投药" => "投藥",
+"抗癌药" => "抗癌藥",
"抗御" => "抗禦",
+"抗药" => "抗藥",
+"抗硷" => "抗鹼",
"折冲" => "折衝",
-"披复" => "披覆",
+"披榛采兰" => "披榛採蘭",
+"披头散发" => "披頭散髮",
"披发" => "披髮",
-"抱朴" => "抱朴",
+"抱朴而长吟兮" => "抱朴而長吟兮",
+"抱素怀朴" => "抱素懷樸",
"抵御" => "抵禦",
-"拆伙" => "拆伙",
-"拆台" => "拆臺",
+"抹干" => "抹乾",
+"抽公签" => "抽公籤",
+"抽签" => "抽籤",
+"抿发" => "抿髮",
+"拆伙" => "拆夥",
"拈须" => "拈鬚",
"拉纤" => "拉縴",
"拉面" => "拉麵",
-"拖吊" => "拖吊",
+"拒人于" => "拒人於",
+"拒于" => "拒於",
+"拓朴" => "拓樸",
"拗别" => "拗彆",
+"拘于" => "拘於",
+"拘泥于" => "拘泥於",
+"拙于" => "拙於",
+"拙朴" => "拙樸",
+"拼命" => "拚命",
+"拼舍" => "拚捨",
+"拼死" => "拚死",
+"拼斗" => "拚鬥",
+"拜讬" => "拜託",
+"括发" => "括髮",
+"拭干" => "拭乾",
"拮据" => "拮据",
+"拿准" => "拿準",
+"拿破仑" => "拿破崙",
+"指手划脚" => "指手劃腳",
"振荡" => "振蕩",
+"捆扎" => "捆紮",
+"捉奸" => "捉姦",
+"捉发" => "捉髮",
"捍御" => "捍禦",
+"捏面人" => "捏麵人",
"舍不得" => "捨不得",
"舍出" => "捨出",
"舍去" => "捨去",
"舍命" => "捨命",
+"舍堕" => "捨墮",
+"舍安就危" => "捨安就危",
+"舍实" => "捨實",
"舍己从人" => "捨己從人",
"舍己救人" => "捨己救人",
"舍己为人" => "捨己為人",
@@ -3285,21 +4213,142 @@ $zh2Hant = array(
"舍身" => "捨身",
"舍车保帅" => "捨車保帥",
"舍近求远" => "捨近求遠",
-"捲发" => "捲髮",
+"卷住" => "捲住",
+"卷来" => "捲來",
+"卷儿" => "捲兒",
+"卷入" => "捲入",
+"卷动" => "捲動",
+"卷去" => "捲去",
+"卷图" => "捲圖",
+"卷土重来" => "捲土重來",
+"卷尺" => "捲尺",
+"卷心菜" => "捲心菜",
+"卷成" => "捲成",
+"卷曲" => "捲曲",
+"卷款逃走" => "捲款逃走",
+"卷毛" => "捲毛",
+"卷烟" => "捲煙",
+"卷筒" => "捲筒",
+"卷帘" => "捲簾",
+"卷纸" => "捲紙",
+"卷缩" => "捲縮",
+"卷舌" => "捲舌",
+"卷菸" => "捲菸",
+"卷袖" => "捲袖",
+"卷走" => "捲走",
+"卷起" => "捲起",
+"卷轴" => "捲軸",
+"卷逃" => "捲逃",
+"卷铺盖" => "捲鋪蓋",
+"卷云" => "捲雲",
+"卷风" => "捲風",
+"卷发" => "捲髮",
"捵面" => "捵麵",
"扫荡" => "掃蕩",
"掌柜" => "掌柜",
"排骨面" => "排骨麵",
"挂帘" => "掛帘",
+"挂钟" => "掛鐘",
"挂面" => "掛麵",
+"采下" => "採下",
+"采伐" => "採伐",
+"采住" => "採住",
+"采信" => "採信",
+"采光" => "採光",
+"采到" => "採到",
+"采制" => "採制",
+"采区" => "採區",
+"采去" => "採去",
+"采取" => "採取",
+"采回" => "採回",
+"采在" => "採在",
+"采好" => "採好",
+"采得" => "採得",
+"采拾" => "採拾",
+"采挖" => "採挖",
+"采掘" => "採掘",
+"采摘" => "採摘",
+"采摭" => "採摭",
+"采择" => "採擇",
+"采撷" => "採擷",
+"采收" => "採收",
+"采料" => "採料",
+"采暖" => "採暖",
+"采桑" => "採桑",
+"采样" => "採樣",
+"采樵人" => "採樵人",
+"采树种" => "採樹種",
+"采气" => "採氣",
+"采油" => "採油",
+"采为" => "採為",
+"采煤" => "採煤",
+"采猎" => "採獵",
+"采珠" => "採珠",
+"采生折割" => "採生折割",
+"采用" => "採用",
+"采的" => "採的",
+"采石" => "採石",
+"采石场" => "採石場",
+"采石厂" => "採石廠",
+"采砂场" => "採砂場",
+"采矿" => "採礦",
+"采种" => "採種",
+"采空区" => "採空區",
+"采空采穗" => "採空採穗",
+"采纳" => "採納",
+"采给" => "採給",
+"采花" => "採花",
+"采芹人" => "採芹人",
+"采茶" => "採茶",
+"采菊" => "採菊",
+"采莲" => "採蓮",
+"采薇" => "採薇",
+"采药" => "採藥",
+"采行" => "採行",
+"采补" => "採補",
+"采访" => "採訪",
+"采证" => "採證",
+"采买" => "採買",
+"采购" => "採購",
+"采办" => "採辦",
+"采运" => "採運",
+"采过" => "採過",
+"采选" => "採選",
+"采金" => "採金",
+"采录" => "採錄",
+"采铁" => "採鐵",
+"采集" => "採集",
+"采风" => "採風",
+"采风问俗" => "採風問俗",
+"采食" => "採食",
+"采盐" => "採鹽",
+"掣签" => "掣籤",
"接着说" => "接著說",
-"提心吊胆" => "提心吊膽",
-"插图卷" => "插圖卷",
-"换吊" => "換吊",
+"推讬" => "推託",
+"提子干" => "提子乾",
+"提心吊胆" => "提心弔膽",
+"提摩太后书" => "提摩太後書",
+"插于" => "插於",
+"插足于" => "插足於",
+"换签" => "換籤",
+"换药" => "換藥",
"换只" => "換隻",
"换发" => "換髮",
+"揩干" => "揩乾",
+"揪采" => "揪採",
+"揭丑" => "揭醜",
+"挥手表" => "揮手表",
+"搋面" => "搋麵",
+"损于" => "損於",
+"搏斗" => "搏鬥",
"摇荡" => "搖蕩",
-"搭伙" => "搭伙",
+"搭干铺" => "搭乾鋪",
+"搭伙" => "搭夥",
+"抢占" => "搶佔",
+"搽药" => "搽藥",
+"摧坚获丑" => "摧堅獲醜",
+"摭采" => "摭採",
+"摸棱" => "摸稜",
"折合" => "摺合",
"折奏" => "摺奏",
"折子" => "摺子",
@@ -3312,438 +4361,1106 @@ $zh2Hant = array(
"折篷" => "摺篷",
"折纸" => "摺紙",
"折裙" => "摺裙",
-"撒布" => "撒佈",
+"捞干" => "撈乾",
+"捞面" => "撈麵",
"撚须" => "撚鬚",
+"撞木钟" => "撞木鐘",
"撞球台" => "撞球檯",
-"擂台" => "擂臺",
+"撞钟" => "撞鐘",
+"撞阵冲军" => "撞陣衝軍",
+"撤并" => "撤併",
+"撤后" => "撤後",
+"拨谷" => "撥穀",
+"播于" => "播於",
+"扑冬" => "撲鼕",
+"擀面" => "擀麵",
+"擅于" => "擅於",
+"击钟" => "擊鐘",
"担仔面" => "擔仔麵",
"担担面" => "擔擔麵",
"担着" => "擔著",
"担负着" => "擔負著",
"据云" => "據云",
+"据干而窥井底" => "據榦而窺井底",
+"挤身于" => "擠身於",
+"擢发" => "擢髮",
"擢发难数" => "擢髮難數",
-"摆布" => "擺佈",
+"擦干" => "擦乾",
+"擦药" => "擦藥",
+"拟于" => "擬於",
+"拧干" => "擰乾",
+"摆钟" => "擺鐘",
+"摄于" => "攝於",
"摄制" => "攝製",
"支干" => "支幹",
"收获" => "收穫",
-"改制" => "改製",
-"攻克" => "攻剋",
+"改征" => "改徵",
+"改于" => "改於",
+"攻占" => "攻佔",
+"放蒙挣" => "放懞掙",
"放荡" => "放蕩",
"放松" => "放鬆",
+"故于" => "故於",
+"敏于" => "敏於",
+"败于" => "敗於",
"叙说着" => "敘說著",
-"散伙" => "散伙",
-"散布" => "散佈",
+"教于" => "教於",
+"敢干" => "敢幹",
+"敢于" => "敢於",
+"散伙" => "散夥",
+"散于" => "散於",
"散荡" => "散蕩",
-"散发" => "散髮",
+"敦朴" => "敦樸",
+"敬挽" => "敬輓",
+"敲钟" => "敲鐘",
"整只" => "整隻",
-"整出" => "整齣",
-"文采" => "文采",
-"斗六" => "斗六",
-"斗南" => "斗南",
-"斗大" => "斗大",
-"斗子" => "斗子",
-"斗室" => "斗室",
-"斗方" => "斗方",
-"斗栱" => "斗栱",
-"斗笠" => "斗笠",
-"斗箕" => "斗箕",
-"斗篷" => "斗篷",
-"斗胆" => "斗膽",
+"敌后" => "敵後",
+"敷药" => "敷藥",
+"数天后" => "數天後",
+"数罪并罚" => "數罪併罰",
+"数与虏确" => "數與虜确",
+"文汇报" => "文匯報",
+"文思泉涌" => "文思泉湧",
"斗转参横" => "斗轉參橫",
-"斗量" => "斗量",
-"斗门" => "斗門",
-"料斗" => "料斗",
-"斯里兰卡" => "斯裡蘭卡",
+"斗哄" => "斗鬨",
+"料斗" => "料鬥",
+"斫雕为朴" => "斫雕為樸",
"新历" => "新曆",
-"断头台" => "斷頭臺",
-"方才" => "方纔",
+"新历史" => "新歷史",
+"新扎" => "新紮",
+"新庄" => "新莊",
+"斲雕为朴" => "斲雕為樸",
+"断后" => "斷後",
+"断发" => "斷髮",
+"方便面" => "方便麵",
+"方几" => "方几",
+"于一役" => "於一役",
+"于七" => "於七",
+"于世" => "於世",
+"于事" => "於事",
+"于事无补" => "於事無補",
+"于人" => "於人",
+"于今" => "於今",
+"于他" => "於他",
+"于伏" => "於伏",
+"于何" => "於何",
+"于你" => "於你",
+"于前" => "於前",
+"于劣" => "於劣",
+"于勤" => "於勤",
+"于呼哀哉" => "於呼哀哉",
+"于国" => "於國",
+"于坏" => "於坏",
+"于垂" => "於垂",
+"于夫罗" => "於夫羅",
+"于她" => "於她",
+"于好" => "於好",
+"于始" => "於始",
+"于它" => "於它",
+"于家" => "於家",
+"于密" => "於密",
+"于左" => "於左",
+"于差" => "於差",
+"于己" => "於己",
+"于市" => "於市",
+"于幕" => "於幕",
+"于幼华" => "於幼華",
+"于弱" => "於弱",
+"于强" => "於強",
+"于征" => "於征",
+"于后" => "於後",
+"于心" => "於心",
+"于心何忍" => "於心何忍",
+"于思" => "於思",
+"于怀" => "於懷",
+"于我" => "於我",
+"于斯" => "於斯",
+"于于" => "於於",
+"于是" => "於是",
+"于时" => "於時",
+"于梨华" => "於梨華",
+"于乐" => "於樂",
+"于此" => "於此",
+"于民" => "於民",
+"于法" => "於法",
+"于法无据" => "於法無據",
+"于潜县" => "於潛縣",
+"于火" => "於火",
+"于焉" => "於焉",
+"于墙" => "於牆",
+"于物" => "於物",
+"于田" => "於田",
+"于毕" => "於畢",
+"于尽" => "於盡",
+"于盲" => "於盲",
+"于祂" => "於祂",
+"于穆" => "於穆",
+"于终" => "於終",
+"于美" => "於美",
+"于色" => "於色",
+"于行" => "於行",
+"于衷" => "於衷",
+"于该" => "於該",
+"于农" => "於農",
+"于途" => "於途",
+"于丑" => "於醜",
+"于野" => "於野",
+"于陆" => "於陸",
+"于飞" => "於飛",
"施舍" => "施捨",
+"施于" => "施於",
+"施舍之道" => "施舍之道",
+"施药" => "施藥",
+"旁征博引" => "旁徵博引",
+"旁注" => "旁註",
+"旅游" => "旅遊",
+"旋干转坤" => "旋乾轉坤",
"旋绕着" => "旋繞著",
"旋回" => "旋迴",
"族里" => "族裡",
+"日后" => "日後",
"日历" => "日曆",
"日志" => "日誌",
-"日进斗金" => "日進斗金",
-"明了" => "明瞭",
+"早于" => "早於",
+"旱干" => "旱乾",
+"昆仑" => "昆崙",
+"升平" => "昇平",
+"升阳" => "昇陽",
+"明征" => "明徵",
+"明于" => "明於",
"明窗净几" => "明窗淨几",
+"明范" => "明範",
"明里" => "明裡",
-"星斗" => "星斗",
+"易于" => "易於",
"星历" => "星曆",
-"星移斗换" => "星移斗換",
-"星移斗转" => "星移斗轉",
-"星罗棋布" => "星羅棋佈",
"星辰表" => "星辰錶",
+"星斗" => "星鬥",
"春假里" => "春假裡",
"春天里" => "春天裡",
+"春药" => "春藥",
+"春游" => "春遊",
+"昧于" => "昧於",
+"时钟" => "時鐘",
"晃荡" => "晃蕩",
+"晋升" => "晉陞",
+"晒干" => "晒乾",
+"晞发" => "晞髮",
+"晨钟" => "晨鐘",
+"晨钟暮鼓" => "晨鐘暮鼓",
"景致" => "景緻",
+"晾干" => "晾乾",
+"晕船药" => "暈船藥",
+"晕车药" => "暈車藥",
"暗地里" => "暗地裡",
"暗沟里" => "暗溝裡",
"暗里" => "暗裡",
+"暗斗" => "暗鬥",
+"畅游" => "暢遊",
+"暂于" => "暫於",
+"暮鼓晨钟" => "暮鼓晨鐘",
+"历元" => "曆元",
+"历命" => "曆命",
+"历始" => "曆始",
+"历室" => "曆室",
+"历尾" => "曆尾",
"历数" => "曆數",
+"历日" => "曆日",
"历书" => "曆書",
+"历本" => "曆本",
"历法" => "曆法",
-"书卷" => "書卷",
+"历纪" => "曆紀",
+"晒干" => "曬乾",
+"晒谷" => "曬穀",
+"更仆难数" => "更僕難數",
+"更签" => "更籤",
+"书后" => "書後",
+"书呆子" => "書獃子",
+"书签" => "書籤",
+"曾于" => "曾於",
+"曾朴" => "曾樸",
+"最后" => "最後",
+"会占" => "會佔",
"会干" => "會幹",
+"会后" => "會後",
+"会于" => "會於",
"会里" => "會裡",
"月历" => "月曆",
-"月台" => "月臺",
+"有仆" => "有僕",
+"有助于" => "有助於",
+"有害于" => "有害於",
+"有损于" => "有損於",
+"有求于人" => "有求於人",
+"有奖征答" => "有獎徵答",
+"有益于" => "有益於",
+"有棱有角" => "有稜有角",
"有只" => "有隻",
+"有余" => "有餘",
+"有发头陀寺" => "有髮頭陀寺",
+"服务于" => "服務於",
+"服从于" => "服從於",
+"服于" => "服於",
+"服药" => "服藥",
+"朝干夕惕" => "朝乾夕惕",
+"朝后" => "朝後",
+"朝钟" => "朝鐘",
+"朦胧" => "朦朧",
+"木偶戏扎" => "木偶戲紮",
"木制" => "木製",
-"本台" => "本臺",
-"朴子" => "朴子",
-"朴实" => "朴實",
-"朴硝" => "朴硝",
-"朴素" => "朴素",
-"朴资茅斯" => "朴資茅斯",
+"未干" => "未乾",
+"末药" => "末藥",
+"术赤" => "朮赤",
+"朱庆余" => "朱慶餘",
+"朱理安历" => "朱理安曆",
+"李连杰" => "李連杰",
+"村庄" => "村莊",
+"村落发" => "村落發",
"村里" => "村裡",
"束发" => "束髮",
+"杯干" => "杯乾",
+"杰特" => "杰特",
"东岳" => "東嶽",
-"东征" => "東征",
-"松赞干布" => "松贊干布",
+"东冲西突" => "東衝西突",
+"东游" => "東遊",
+"松山庄" => "松山庄",
+"松柏后凋" => "松柏後凋",
"板着脸" => "板著臉",
"板荡" => "板蕩",
-"枕借" => "枕藉",
+"枕经藉史" => "枕經藉史",
+"枕藉" => "枕藉",
"林宏岳" => "林宏嶽",
+"林郁方" => "林郁方",
+"林钟" => "林鐘",
+"果干" => "果乾",
+"果子干" => "果子乾",
+"果于" => "果於",
+"枝不得大于干" => "枝不得大於榦",
"枝干" => "枝幹",
-"枯干" => "枯幹",
+"枯干" => "枯乾",
"某只" => "某隻",
+"染指于" => "染指於",
"染发" => "染髮",
"柜上" => "柜上",
"柜台" => "柜台",
"柜子" => "柜子",
-"查卷" => "查卷",
-"查号台" => "查號臺",
+"柱梁" => "柱樑",
+"校准" => "校準",
"校雠学" => "校讎學",
-"核准" => "核准",
-"核复" => "核覆",
-"格里" => "格裡",
-"案卷" => "案卷",
+"核准的" => "核准的",
+"格于" => "格於",
+"格范" => "格範",
+"格斗" => "格鬥",
+"桂圆干" => "桂圓乾",
+"案发后" => "案發後",
+"桌历" => "桌曆",
+"桑干" => "桑乾",
"条干" => "條幹",
-"棉卷" => "棉卷",
+"梨干" => "梨乾",
+"械斗" => "械鬥",
+"弃舍" => "棄捨",
"棉制" => "棉製",
+"棒子面" => "棒子麵",
+"枣庄" => "棗莊",
+"栋梁" => "棟樑",
+"棫朴" => "棫樸",
+"栖于" => "棲於",
"植发" => "植髮",
-"楼台" => "樓臺",
+"椰枣干" => "椰棗乾",
+"楚庄王" => "楚莊王",
+"桢干" => "楨幹",
+"业余" => "業餘",
+"榨干" => "榨乾",
+"乐意于" => "樂意於",
+"乐于" => "樂於",
+"樊于期" => "樊於期",
+"梁上" => "樑上",
+"梁子" => "樑子",
+"梁书" => "樑書",
+"梁柱" => "樑柱",
"标志着" => "標志著",
+"标标致致" => "標標致致",
+"标准" => "標準",
+"标签" => "標籤",
"标致" => "標緻",
"标志" => "標誌",
+"模棱" => "模稜",
+"模范" => "模範",
+"模范棒棒堂" => "模范棒棒堂",
"模制" => "模製",
+"样范" => "樣範",
+"樵采" => "樵採",
+"朴修斯" => "樸修斯",
+"朴厚" => "樸厚",
+"朴学" => "樸學",
+"朴实" => "樸實",
+"朴念仁" => "樸念仁",
+"朴拙" => "樸拙",
+"朴樕" => "樸樕",
+"朴父" => "樸父",
+"朴直" => "樸直",
+"朴素" => "樸素",
+"朴讷" => "樸訥",
+"朴质" => "樸質",
+"朴鄙" => "樸鄙",
+"朴重" => "樸重",
+"朴野" => "樸野",
+"朴野无文" => "樸野無文",
+"朴钝" => "樸鈍",
+"朴陋" => "樸陋",
+"朴马" => "樸馬",
+"朴鲁" => "樸魯",
"树干" => "樹幹",
-"横征暴敛" => "橫征暴斂",
+"树干" => "樹榦",
+"树梁" => "樹樑",
+"桥梁" => "橋樑",
+"机绣" => "機繡",
+"横征暴敛" => "橫徵暴斂",
+"横梁" => "橫樑",
"横冲" => "橫衝",
-"档卷" => "檔卷",
-"检复" => "檢覆",
"台子" => "檯子",
"台布" => "檯布",
"台灯" => "檯燈",
"台球" => "檯球",
"台面" => "檯面",
"柜台" => "櫃檯",
-"柜台" => "櫃臺",
-"栏干" => "欄干",
+"栉发工" => "櫛髮工",
+"次于" => "次於",
"欺蒙" => "欺矇",
-"歌后" => "歌后",
-"欧几里得" => "歐幾裡得",
+"歇后" => "歇後",
+"歌钟" => "歌鐘",
+"欧游" => "歐遊",
+"止咳药" => "止咳藥",
+"止于" => "止於",
+"止痛药" => "止痛藥",
+"止血药" => "止血藥",
+"正官庄" => "正官庄",
+"正后" => "正後",
+"正于" => "正於",
"正当着" => "正當著",
-"武后" => "武后",
-"武松" => "武鬆",
+"此后" => "此後",
+"武斗" => "武鬥",
"归并" => "歸併",
+"归功于" => "歸功於",
+"归咎于" => "歸咎於",
+"归因于" => "歸因於",
+"归于" => "歸於",
+"归罪于" => "歸罪於",
+"归随于" => "歸隨於",
+"归顺于" => "歸順於",
+"归余" => "歸餘",
+"死伤相藉" => "死傷相藉",
+"死后" => "死後",
+"死于" => "死於",
"死里求生" => "死裡求生",
"死里逃生" => "死裡逃生",
-"残卷" => "殘卷",
+"殖谷" => "殖穀",
+"残余" => "殘餘",
"杀虫药" => "殺虫藥",
+"杀虫药" => "殺蟲藥",
"壳里" => "殼裡",
-"母后" => "母后",
+"殿后" => "殿後",
+"殿钟自鸣" => "殿鐘自鳴",
+"毁于" => "毀於",
+"毁钟为铎" => "毀鐘為鐸",
+"母范" => "母範",
+"每于" => "每於",
"每只" => "每隻",
-"比干" => "比干",
-"毛卷" => "毛卷",
+"毒药" => "毒藥",
+"比划" => "比劃",
+"毛姜" => "毛薑",
"毛发" => "毛髮",
+"毫厘" => "毫釐",
"毫发" => "毫髮",
-"气冲牛斗" => "氣沖牛斗",
-"气象台" => "氣象臺",
+"气郁" => "氣鬱",
+"氤郁" => "氤鬱",
"氯霉素" => "氯黴素",
-"水斗" => "水斗",
+"水准" => "水準",
"水里" => "水裡",
+"水里乡" => "水里鄉",
"水表" => "水錶",
+"水硷" => "水鹼",
"永历" => "永曆",
+"求助于" => "求助於",
+"求教于" => "求教於",
+"求知欲" => "求知慾",
+"求签" => "求籤",
+"汗硷" => "汗鹼",
"污蔑" => "汙衊",
"池里" => "池裡",
"污蔑" => "污衊",
+"汲于" => "汲於",
+"汲汲于" => "汲汲於",
+"决斗" => "決鬥",
+"沈淀" => "沈澱",
"沈着" => "沈著",
+"沈郁" => "沈鬱",
+"沉湎于" => "沉湎於",
+"沉溺于" => "沉溺於",
+"沉淀" => "沉澱",
+"沉迷于" => "沉迷於",
+"沉醉于" => "沉醉於",
+"沉郁" => "沉鬱",
+"没干没净" => "沒乾沒淨",
"没事干" => "沒事幹",
-"没精打采" => "沒精打采",
+"没干" => "沒幹",
+"没梢干" => "沒梢幹",
+"没样范" => "沒樣範",
+"没药" => "沒藥",
+"冲冠发怒" => "沖冠髮怒",
"冲着" => "沖著",
"沙里淘金" => "沙裡淘金",
+"河流汇集" => "河流匯集",
"河里" => "河裡",
+"油漆未干" => "油漆未乾",
"油面" => "油麵",
+"泛游" => "泛遊",
"泡面" => "泡麵",
-"泰斗" => "泰斗",
+"波棱菜" => "波稜菜",
+"波发藻" => "波髮藻",
+"泥于" => "泥於",
+"注云" => "注云",
+"泱郁" => "泱鬱",
+"泳气钟" => "泳氣鐘",
+"洄游" => "洄遊",
+"洋面" => "洋麵",
"洗手不干" => "洗手不幹",
+"洗发" => "洗髮",
"洗发精" => "洗髮精",
+"洛钟东应" => "洛鐘東應",
+"洪范" => "洪範",
+"洪钟" => "洪鐘",
+"汹涌" => "洶湧",
+"活动于" => "活動於",
"派团参加" => "派團參加",
+"流于" => "流於",
"流荡" => "流蕩",
+"流行于" => "流行於",
"浩荡" => "浩蕩",
"浪琴表" => "浪琴錶",
"浪荡" => "浪蕩",
+"浮于" => "浮於",
"浮荡" => "浮蕩",
-"海里" => "海裡",
+"浮游" => "浮遊",
+"浮松" => "浮鬆",
+"海干" => "海乾",
+"浸于" => "浸於",
"涂着" => "涂著",
+"涂谨申" => "涂謹申",
+"消炎药" => "消炎藥",
+"消肿药" => "消腫藥",
+"涉足于" => "涉足於",
"液晶表" => "液晶錶",
+"涳蒙" => "涳濛",
+"涸干" => "涸乾",
"凉面" => "涼麵",
+"淑范" => "淑範",
+"泪干" => "淚乾",
+"泪如泉涌" => "淚如泉湧",
+"淡于" => "淡於",
+"淡蒙蒙" => "淡濛濛",
"淡朱" => "淡硃",
+"净余" => "淨餘",
+"净发" => "淨髮",
+"淫欲" => "淫慾",
"淫荡" => "淫蕩",
-"测验卷" => "測驗卷",
+"深于" => "深於",
+"淳朴" => "淳樸",
"港制" => "港製",
"游荡" => "游蕩",
+"浑朴" => "渾樸",
"凑合着" => "湊合著",
"湖里" => "湖裡",
+"湘绣" => "湘繡",
+"湘累" => "湘纍",
+"湟潦生苹" => "湟潦生苹",
+"涌上" => "湧上",
+"涌来" => "湧來",
+"涌入" => "湧入",
+"涌出" => "湧出",
+"涌向" => "湧向",
+"涌泉" => "湧泉",
+"涌现" => "湧現",
+"涌起" => "湧起",
+"涌进" => "湧進",
+"湮郁" => "湮鬱",
+"汤下面" => "湯下麵",
"汤团" => "湯糰",
+"汤药" => "湯藥",
"汤面" => "湯麵",
+"源于" => "源於",
+"准备" => "準備",
+"准儿" => "準兒",
+"准则" => "準則",
+"准噶尔" => "準噶爾",
+"准定" => "準定",
+"准平原" => "準平原",
+"准度" => "準度",
+"准据" => "準據",
+"准新娘" => "準新娘",
+"准新郎" => "準新郎",
+"准星" => "準星",
+"准是" => "準是",
+"准时" => "準時",
+"准会" => "準會",
+"准决赛" => "準決賽",
+"准的" => "準的",
+"准确" => "準確",
+"准线" => "準線",
+"准绳" => "準繩",
+"准话" => "準話",
+"准头" => "準頭",
+"溟蒙" => "溟濛",
+"溢于" => "溢於",
+"溢于言表" => "溢於言表",
+"溲面" => "溲麵",
+"溶于" => "溶於",
+"溺于" => "溺於",
+"滃郁" => "滃鬱",
+"滑藉" => "滑藉",
+"汇丰" => "滙豐",
+"卤味" => "滷味",
+"卤水" => "滷水",
+"卤汁" => "滷汁",
+"卤湖" => "滷湖",
+"卤肉" => "滷肉",
+"卤菜" => "滷菜",
+"卤蛋" => "滷蛋",
"卤制" => "滷製",
+"卤鸡" => "滷雞",
"卤面" => "滷麵",
-"满布" => "滿佈",
+"满于" => "滿於",
+"满满当当" => "滿滿當當",
"漂荡" => "漂蕩",
-"漏斗" => "漏斗",
-"演奏台" => "演奏臺",
+"沤郁" => "漚鬱",
+"汉弥登钟" => "漢彌登鐘",
+"漫游" => "漫遊",
+"潜水钟" => "潛水鐘",
"潭里" => "潭裡",
+"潮涌" => "潮湧",
+"溃于" => "潰於",
+"澄澹精致" => "澄澹精致",
+"澒蒙" => "澒濛",
+"泽渗漓而下降" => "澤滲灕而下降",
+"淀粉" => "澱粉",
+"澹台" => "澹臺",
+"激于" => "激於",
"激荡" => "激蕩",
-"浓郁" => "濃郁",
+"浓于" => "濃於",
"浓发" => "濃髮",
-"湿地松" => "濕地鬆",
-"蒙蒙" => "濛濛",
+"蒙汜" => "濛汜",
+"蒙蒙细雨" => "濛濛細雨",
"蒙雾" => "濛霧",
-"瀛台" => "瀛臺",
+"蒙松雨" => "濛鬆雨",
+"泻药" => "瀉藥",
+"沈吉线" => "瀋吉線",
+"沈山线" => "瀋山線",
+"沈州" => "瀋州",
+"沈水" => "瀋水",
+"沈河" => "瀋河",
+"沈海" => "瀋海",
+"沈海铁路" => "瀋海鐵路",
+"沈阳" => "瀋陽",
+"濒于" => "瀕於",
+"弥山遍野" => "瀰山遍野",
"弥漫" => "瀰漫",
"弥漫着" => "瀰漫著",
+"弥弥" => "瀰瀰",
+"灌于" => "灌於",
+"灌药" => "灌藥",
+"漓湘" => "灕湘",
+"漓然" => "灕然",
"火并" => "火併",
+"火签" => "火籤",
+"火药" => "火藥",
"灰蒙" => "灰濛",
+"灰蒙蒙" => "灰濛濛",
+"炆面" => "炆麵",
"炒面" => "炒麵",
"炮制" => "炮製",
-"炸药" => "炸葯",
+"炸药" => "炸藥",
"炸酱面" => "炸醬麵",
+"为准" => "為準",
"为着" => "為著",
-"乌干达" => "烏干達",
-"乌苏里江" => "烏蘇裡江",
"乌发" => "烏髮",
"乌龙面" => "烏龍麵",
+"烘干" => "烘乾",
"烘制" => "烘製",
-"烽火台" => "烽火臺",
-"无干" => "無干",
-"无精打采" => "無精打采",
+"烤干" => "烤乾",
+"焙干" => "焙乾",
+"无助于" => "無助於",
+"无动于衷" => "無動於衷",
+"无可救药" => "無可救藥",
+"无后" => "無後",
+"无损于" => "無損於",
+"无梁楼盖" => "無樑樓蓋",
+"无济于事" => "無濟於事",
+"无畏于" => "無畏於",
+"无补于事" => "無補於事",
+"无视于" => "無視於",
+"无余" => "無餘",
+"然后" => "然後",
+"然身死才数月耳" => "然身死纔數月耳",
+"炼药" => "煉藥",
"炼制" => "煉製",
-"烟卷儿" => "煙卷兒",
-"烟斗" => "煙斗",
+"煎药" => "煎藥",
+"煎面" => "煎麵",
+"烟卷" => "煙捲",
"烟斗丝" => "煙斗絲",
-"烟台" => "煙臺",
-"照准" => "照准",
-"熨斗" => "熨斗",
-"灯台" => "燈臺",
+"烟斗" => "煙鬥",
+"烟硷" => "煙鹼",
+"照占" => "照佔",
+"照入签" => "照入籤",
+"照准" => "照準",
+"煨干" => "煨乾",
+"煮面" => "煮麵",
+"熔于" => "熔於",
+"熨斗" => "熨鬥",
+"熬药" => "熬藥",
+"热衷于" => "熱衷於",
+"炖药" => "燉藥",
"燎发" => "燎髮",
+"烧干" => "燒乾",
+"烧硷" => "燒鹼",
+"燕几" => "燕几",
+"燕游" => "燕遊",
"烫发" => "燙髮",
"烫面" => "燙麵",
-"烛台" => "燭臺",
-"炉台" => "爐臺",
+"营干" => "營幹",
+"烬余" => "燼餘",
+"争先恐后" => "爭先恐後",
+"争奇斗艳" => "爭奇鬥艷",
+"争奇斗豔" => "爭奇鬥豔",
+"争妍斗胜" => "爭妍鬥勝",
+"争妍斗豔" => "爭妍鬥豔",
+"争斗" => "爭鬥",
+"爰定祥历" => "爰定祥厤",
"爽荡" => "爽蕩",
+"尔冬升" => "爾冬陞",
+"尔后" => "爾後",
"片言只语" => "片言隻語",
+"牙签" => "牙籤",
"牛肉面" => "牛肉麵",
"牛只" => "牛隻",
-"特准" => "特准",
-"特征" => "特征",
-"特里" => "特裡",
+"物欲" => "物慾",
+"特征" => "特徵",
+"特效药" => "特效藥",
+"特于" => "特於",
"特制" => "特製",
+"牵一发" => "牽一髮",
"牵系" => "牽繫",
-"狼借" => "狼藉",
+"荦确" => "犖确",
+"狂并潮" => "狂併潮",
+"狃于" => "狃於",
+"狃于成见" => "狃於成見",
+"狐藉虎威" => "狐藉虎威",
+"狼藉" => "狼藉",
+"猛于" => "猛於",
"猛冲" => "猛衝",
+"猜三划五" => "猜三划五",
"奖杯" => "獎盃",
-"获准" => "獲准",
+"独占" => "獨佔",
+"独占鳌头" => "獨佔鰲頭",
+"兽欲" => "獸慾",
+"献丑" => "獻醜",
"率团参加" => "率團參加",
-"王侯后" => "王侯后",
-"王后" => "王后",
+"玉历" => "玉曆",
+"王庄" => "王莊",
+"王余鱼" => "王餘魚",
"班里" => "班裡",
"理发" => "理髮",
-"瑶台" => "瑤臺",
+"琴钟" => "琴鐘",
+"瑶签" => "瑤籤",
+"环游" => "環遊",
+"甘于" => "甘於",
+"甚于" => "甚於",
"甚么" => "甚麼",
+"甜水面" => "甜水麵",
"甜面酱" => "甜麵醬",
"生力面" => "生力麵",
+"生于" => "生於",
+"生姜" => "生薑",
"生锈" => "生鏽",
"生发" => "生髮",
+"产后" => "產後",
+"用于" => "用於",
+"用药" => "用藥",
+"甩发" => "甩髮",
+"田谷" => "田穀",
+"田庄" => "田莊",
"田里" => "田裡",
-"由馀" => "由余",
-"男佣" => "男佣",
+"由于" => "由於",
+"男仆" => "男僕",
"男用表" => "男用錶",
+"畏于" => "畏於",
+"留后" => "留後",
"留发" => "留髮",
-"畚斗" => "畚斗",
+"毕于" => "畢於",
+"毕业于" => "畢業於",
+"毕生发展" => "畢生發展",
+"划着" => "畫著",
+"异于" => "異於",
+"当一天和尚撞一天钟" => "當一天和尚撞一天鐘",
+"当家才知柴米价" => "當家纔知柴米價",
+"当于" => "當於",
+"当当丁丁" => "當當丁丁",
"当着" => "當著",
+"疏于" => "疏於",
"疏松" => "疏鬆",
+"疑凶" => "疑兇",
+"疲于" => "疲於",
+"疲于奔命" => "疲於奔命",
"疲困" => "疲睏",
+"病后" => "病後",
+"病征" => "病徵",
"病症" => "病癥",
"症候" => "癥候",
"症状" => "癥狀",
"症结" => "癥結",
-"登台" => "登臺",
-"发布" => "發佈",
+"发干" => "發乾",
+"发于" => "發於",
+"发汗药" => "發汗藥",
+"发呆" => "發獃",
+"发签" => "發籤",
"发着" => "發著",
+"发松" => "發鬆",
"发面" => "發麵",
-"发霉" => "發黴",
-"白卷" => "白卷",
-"白干儿" => "白干兒",
+"白干" => "白乾",
+"白术" => "白朮",
+"白朴" => "白樸",
+"白发其事" => "白發其事",
+"白粉面" => "白粉麵",
"白发" => "白髮",
-"白面" => "白麵",
-"百里" => "百裡",
+"白霉" => "白黴",
+"百多只" => "百多隻",
+"百天后" => "百天後",
+"百拙千丑" => "百拙千醜",
+"百扎" => "百紮",
+"百花历" => "百花曆",
"百只" => "百隻",
-"皇后" => "皇后",
"皇历" => "皇曆",
+"皇极历" => "皇極曆",
+"皇庄" => "皇莊",
"皓发" => "皓髮",
"皮里阳秋" => "皮裏陽秋",
"皮里春秋" => "皮裡春秋",
"皮制" => "皮製",
+"皮松" => "皮鬆",
+"皱别" => "皺彆",
"皱折" => "皺摺",
+"盈余" => "盈餘",
+"益于" => "益於",
"盒里" => "盒裡",
+"盛德遗范" => "盛德遺範",
+"盛行于" => "盛行於",
+"盛赞" => "盛讚",
+"盗采" => "盜採",
+"盗钟" => "盜鐘",
"监制" => "監製",
"盘里" => "盤裡",
"盘回" => "盤迴",
+"卢棱伽" => "盧稜伽",
+"盲干" => "盲幹",
"直接参与" => "直接參与",
+"直于" => "直於",
"直冲" => "直衝",
+"相并" => "相併",
"相克" => "相剋",
+"相同于" => "相同於",
"相干" => "相干",
+"相于" => "相於",
"相冲" => "相衝",
-"看台" => "看臺",
+"相斗" => "相鬥",
+"看准" => "看準",
+"真凶" => "真兇",
"眼帘" => "眼帘",
"眼眶里" => "眼眶裡",
+"眼药" => "眼藥",
"眼里" => "眼裡",
"困乏" => "睏乏",
+"困觉" => "睏覺",
"睡着了" => "睡著了",
-"了如" => "瞭如",
+"瞄准" => "瞄準",
+"瞠乎后矣" => "瞠乎後矣",
"了望" => "瞭望",
"了然" => "瞭然",
"了若指掌" => "瞭若指掌",
-"了解" => "瞭解",
-"蒙住" => "矇住",
+"瞻前顾后" => "瞻前顧後",
+"蒙事" => "矇事",
"蒙昧无知" => "矇昧無知",
+"蒙松雨" => "矇松雨",
"蒙混" => "矇混",
-"蒙蒙" => "矇矇",
+"蒙瞍" => "矇瞍",
"蒙眬" => "矇矓",
-"蒙蔽" => "矇蔽",
+"蒙聩" => "矇聵",
+"蒙着" => "矇著",
+"蒙着锅儿" => "矇著鍋兒",
+"蒙头转" => "矇頭轉",
"蒙骗" => "矇騙",
+"瞩讬" => "矚託",
+"短于" => "短於",
"短发" => "短髮",
+"石棱棱" => "石稜稜",
"石英表" => "石英錶",
+"石钟乳" => "石鐘乳",
+"石硷" => "石鹼",
"研制" => "研製",
"砰当" => "砰噹",
-"砲台" => "砲臺",
"朱唇皓齿" => "硃唇皓齒",
"朱批" => "硃批",
"朱砂" => "硃砂",
"朱笔" => "硃筆",
"朱红色" => "硃紅色",
"朱色" => "硃色",
+"硫化硷" => "硫化鹼",
"硬干" => "硬幹",
-"砚台" => "硯臺",
+"确瘠" => "确瘠",
"碑志" => "碑誌",
+"碰钟" => "碰鐘",
"磁制" => "磁製",
"磨制" => "磨製",
-"示复" => "示覆",
+"硗确" => "磽确",
+"碍于" => "礙於",
+"砻谷机" => "礱穀機",
+"示范" => "示範",
"社里" => "社裡",
-"神采" => "神采",
+"祝发" => "祝髮",
+"神荼郁垒" => "神荼鬱壘",
+"神游" => "神遊",
+"神雕像" => "神雕像",
+"神雕" => "神鵰",
+"祭吊" => "祭弔",
+"禁欲" => "禁慾",
+"禁药" => "禁藥",
+"祸于" => "禍於",
"御侮" => "禦侮",
"御寇" => "禦寇",
"御寒" => "禦寒",
"御敌" => "禦敵",
+"礼赞" => "禮讚",
+"禾谷" => "禾穀",
+"秃妃之发" => "禿妃之髮",
"秃发" => "禿髮",
"秀发" => "秀髮",
"私下里" => "私下裡",
+"私欲" => "私慾",
+"私斗" => "私鬥",
"秋天里" => "秋天裡",
+"秋后" => "秋後",
"秋裤" => "秋褲",
+"秋游" => "秋遊",
+"秋阴入井干" => "秋陰入井幹",
+"秋发" => "秋髮",
+"种师中" => "种師中",
+"种师道" => "种師道",
+"种放" => "种放",
+"科范" => "科範",
"秒表" => "秒錶",
+"秒钟" => "秒鐘",
"稀松" => "稀鬆",
-"禀复" => "稟覆",
+"稍后" => "稍後",
+"棱台" => "稜台",
+"棱子" => "稜子",
+"棱层" => "稜層",
+"棱柱" => "稜柱",
+"棱棱" => "稜稜",
+"棱棱睁睁" => "稜稜睜睜",
+"棱等登" => "稜等登",
+"棱线" => "稜線",
+"棱缝" => "稜縫",
+"棱角" => "稜角",
+"棱锥" => "稜錐",
+"棱镜" => "稜鏡",
+"棱体" => "稜體",
+"种谷" => "種穀",
+"称赞" => "稱讚",
"稻谷" => "稻穀",
-"稽征" => "稽征",
+"稽征" => "稽徵",
"谷仓" => "穀倉",
+"谷圭" => "穀圭",
"谷场" => "穀場",
"谷子" => "穀子",
+"谷日" => "穀日",
+"谷旦" => "穀旦",
"谷壳" => "穀殼",
"谷物" => "穀物",
"谷皮" => "穀皮",
"谷神" => "穀神",
+"谷米" => "穀米",
"谷粒" => "穀粒",
"谷舱" => "穀艙",
"谷苗" => "穀苗",
"谷草" => "穀草",
+"谷贵饿农" => "穀貴餓農",
"谷贱伤农" => "穀賤傷農",
"谷道" => "穀道",
"谷雨" => "穀雨",
"谷类" => "穀類",
+"谷食" => "穀食",
+"穆罕默德历" => "穆罕默德曆",
"积极参与" => "積极參与",
"积极参加" => "積极參加",
+"积谷" => "積穀",
+"积郁" => "積鬱",
+"稳扎" => "穩紮",
"空荡" => "空蕩",
+"空钟" => "空鐘",
+"空余" => "空餘",
"窗帘" => "窗帘",
"窗明几净" => "窗明几淨",
"窗台" => "窗檯",
-"窗台" => "窗臺",
"窝里" => "窩裡",
-"窝阔台" => "窩闊臺",
+"穷于" => "窮於",
"穷追不舍" => "窮追不捨",
-"笆斗" => "笆斗",
+"窃钟掩耳" => "竊鐘掩耳",
+"立于" => "立於",
+"立范" => "立範",
+"站干岸儿" => "站乾岸兒",
+"竟于" => "竟於",
+"童仆" => "童僕",
+"端庄" => "端莊",
+"竞斗" => "競鬥",
+"竹签" => "竹籤",
+"笆斗" => "笆鬥",
"笑里藏刀" => "笑裡藏刀",
-"第一卷" => "第一卷",
-"筋斗" => "筋斗",
-"答卷" => "答卷",
-"答复" => "答複",
+"笔划" => "筆劃",
+"等同于" => "等同於",
+"等于" => "等於",
+"筋斗" => "筋鬥",
+"笋干" => "筍乾",
+"筑前" => "筑前",
+"筑北" => "筑北",
+"筑州" => "筑州",
+"筑后" => "筑後",
+"筑波" => "筑波",
+"筑紫" => "筑紫",
+"筑肥" => "筑肥",
+"筑西" => "筑西",
+"筑邦" => "筑邦",
+"筑阳" => "筑陽",
"答复" => "答覆",
+"策划" => "策劃",
"筵几" => "筵几",
-"箕斗" => "箕斗",
+"箕斗" => "箕鬥",
+"算发" => "算髮",
+"管干" => "管幹",
+"节欲" => "節慾",
+"节余" => "節餘",
+"范例" => "範例",
+"范围" => "範圍",
+"范式" => "範式",
+"范文" => "範文",
+"范本" => "範本",
+"范畴" => "範疇",
+"简朴" => "簡樸",
"签着" => "簽著",
+"筹划" => "籌劃",
+"签幐" => "籤幐",
+"签押" => "籤押",
+"签条" => "籤條",
+"签诗" => "籤詩",
+"吁天" => "籲天",
"吁求" => "籲求",
"吁请" => "籲請",
+"米谷" => "米穀",
+"粉拳绣腿" => "粉拳繡腿",
+"粉签子" => "粉籤子",
"粗制" => "粗製",
-"粗卤" => "粗鹵",
"精干" => "精幹",
+"精采" => "精採",
+"精于" => "精於",
"精明强干" => "精明強幹",
+"精准" => "精準",
"精致" => "精緻",
"精制" => "精製",
-"精辟" => "精辟",
-"精采" => "精采",
+"精通于" => "精通於",
+"精辟" => "精闢",
+"精松" => "精鬆",
"糊里糊涂" => "糊裡糊塗",
+"糕干" => "糕乾",
+"粪秽蔑面" => "糞穢衊面",
"团子" => "糰子",
"系着" => "系著",
+"纪元后" => "紀元後",
"纪历" => "紀曆",
"红发" => "紅髮",
"红霉素" => "紅黴素",
"纡回" => "紆迴",
-"纳采" => "納采",
+"纡余" => "紆餘",
+"纡郁" => "紆鬱",
+"纯朴" => "純樸",
+"纯硷" => "純鹼",
+"纸扎" => "紙紮",
+"素朴" => "素樸",
+"素藉" => "素藉",
"素食面" => "素食麵",
"素面" => "素麵",
-"紫微斗数" => "紫微斗數",
+"索面" => "索麵",
+"紫姜" => "紫薑",
+"扎上" => "紮上",
+"扎下" => "紮下",
+"扎好" => "紮好",
+"扎实" => "紮實",
+"扎寨" => "紮寨",
+"扎带子" => "紮帶子",
+"扎成" => "紮成",
+"扎根" => "紮根",
+"扎营" => "紮營",
+"扎紧" => "紮緊",
+"扎起" => "紮起",
+"扎铁" => "紮鐵",
+"细不容发" => "細不容髮",
"细致" => "細緻",
+"终于" => "終於",
"组里" => "組裡",
+"结伴同游" => "結伴同遊",
+"结伙" => "結夥",
+"结扎" => "結紮",
+"结余" => "結餘",
"结发" => "結髮",
"绝对参照" => "絕對參照",
+"绝后" => "絕後",
+"绝于" => "絕於",
+"绞干" => "絞乾",
+"络绎于途" => "絡繹於途",
+"给于" => "給於",
"丝来线去" => "絲來線去",
"丝布" => "絲布",
+"丝恩发怨" => "絲恩髮怨",
"丝板" => "絲板",
"丝瓜布" => "絲瓜布",
"丝绒布" => "絲絨布",
"丝线" => "絲線",
"丝织厂" => "絲織廠",
"丝虫" => "絲蟲",
-"綑吊" => "綑吊",
-"经卷" => "經卷",
+"丝发" => "絲髮",
+"绑扎" => "綁紮",
+"綑扎" => "綑紮",
+"绿发" => "綠髮",
"绿霉素" => "綠黴素",
+"绸缎庄" => "綢緞莊",
"维系" => "維繫",
"绾发" => "綰髮",
"网里" => "網裡",
+"网志" => "網誌",
"紧绷" => "緊繃",
"紧绷着" => "緊繃著",
"紧追不舍" => "緊追不捨",
+"绪余" => "緒餘",
+"缉凶" => "緝兇",
"编制" => "編製",
+"编钟" => "編鐘",
+"编余" => "編餘",
"编发" => "編髮",
+"缓征" => "緩徵",
"缓冲" => "緩衝",
"致密" => "緻密",
"萦回" => "縈迴",
+"缜致" => "縝緻",
"县里" => "縣裡",
"县志" => "縣誌",
"缝里" => "縫裡",
"缝制" => "縫製",
+"缩栗" => "縮慄",
+"纵欲" => "縱慾",
"纤夫" => "縴夫",
"繁复" => "繁複",
"绷住" => "繃住",
@@ -3755,6 +5472,18 @@ $zh2Hant = array(
"绷着脸" => "繃著臉",
"绷着脸儿" => "繃著臉兒",
"绷开" => "繃開",
+"繐帏飘井干" => "繐幃飄井幹",
+"绕梁" => "繞樑",
+"绣得" => "繡得",
+"绣房" => "繡房",
+"绣毯" => "繡毯",
+"绣球" => "繡球",
+"绣的" => "繡的",
+"绣花" => "繡花",
+"绣衣" => "繡衣",
+"绣起" => "繡起",
+"绣阁" => "繡閣",
+"绣鞋" => "繡鞋",
"绘制" => "繪製",
"系上" => "繫上",
"系到" => "繫到",
@@ -3762,115 +5491,186 @@ $zh2Hant = array(
"系心" => "繫心",
"系念" => "繫念",
"系怀" => "繫懷",
-"系数" => "繫數",
"系于" => "繫於",
"系系" => "繫系",
"系紧" => "繫緊",
"系绳" => "繫繩",
"系着" => "繫著",
"系辞" => "繫辭",
-"缴卷" => "繳卷",
"累囚" => "纍囚",
-"累累" => "纍纍",
+"累堆" => "纍堆",
+"累瓦结绳" => "纍瓦結繩",
+"累绁" => "纍紲",
+"累臣" => "纍臣",
+"缠斗" => "纏鬥",
+"才则" => "纔則",
+"才可容颜十五余" => "纔可容顏十五餘",
+"才得两年" => "纔得兩年",
+"才此" => "纔此",
"坛子" => "罈子",
"坛坛罐罐" => "罈罈罐罐",
+"坛騞" => "罈騞",
+"置于" => "置於",
"骂着" => "罵著",
+"美仑" => "美崙",
+"美于" => "美於",
"美制" => "美製",
+"美丑" => "美醜",
"美发" => "美髮",
-"翻来复去" => "翻來覆去",
-"翻天复地" => "翻天覆地",
-"翻复" => "翻覆",
+"羞于" => "羞於",
+"群丑" => "群醜",
+"羨余" => "羨餘",
+"义仆" => "義僕",
+"义形于色" => "義形於色",
+"义庄" => "義莊",
+"习于" => "習於",
+"翕辟" => "翕闢",
+"翱游" => "翱遊",
+"翻涌" => "翻湧",
"翻云复雨" => "翻雲覆雨",
-"老么" => "老么",
+"翻松" => "翻鬆",
+"老干" => "老乾",
+"老仆" => "老僕",
+"老干部" => "老幹部",
+"老蒙" => "老懞",
+"老于" => "老於",
+"老于世故" => "老於世故",
+"老庄" => "老莊",
+"老姜" => "老薑",
"老板" => "老闆",
-"考卷" => "考卷",
+"考后" => "考後",
+"而后" => "而後",
+"而于" => "而於",
+"耐硷" => "耐鹼",
+"耕佣" => "耕傭",
"耕获" => "耕穫",
-"聊斋志异" => "聊齋誌異",
+"耳后" => "耳後",
+"耳余" => "耳餘",
+"耽于" => "耽於",
+"耿于" => "耿於",
+"耿耿于怀" => "耿耿於懷",
+"聊斋志异" => "聊齋志異",
"联系" => "聯係",
+"联于" => "聯於",
"联系" => "聯繫",
+"声如洪钟" => "聲如洪鐘",
+"听于" => "聽於",
+"肉干" => "肉乾",
+"肉欲" => "肉慾",
"肉丝面" => "肉絲麵",
"肉羹面" => "肉羹麵",
"肉松" => "肉鬆",
-"肢体" => "肢体",
+"肝郁" => "肝鬱",
+"股栗" => "股慄",
+"肥筑方言" => "肥筑方言",
+"胃药" => "胃藥",
"背向着" => "背向著",
"背地里" => "背地裡",
+"背后" => "背後",
+"胎发" => "胎髮",
+"胜肽" => "胜肽",
+"胜键" => "胜鍵",
+"胡云" => "胡云",
+"胡朴安" => "胡樸安",
"胡里胡涂" => "胡裡胡塗",
"能干" => "能幹",
"脉冲" => "脈衝",
+"脊梁" => "脊樑",
+"脱谷机" => "脫穀機",
"脱发" => "脫髮",
"腊味" => "腊味",
"腊笔" => "腊筆",
-"腊肉" => "腊肉",
+"腐干" => "腐乾",
"脑子里" => "腦子裡",
+"脑干" => "腦幹",
+"脑后" => "腦後",
"腰里" => "腰裡",
-"胶卷" => "膠卷",
+"脚注" => "腳註",
+"膏药" => "膏藥",
+"臣仆" => "臣僕",
+"臣服于" => "臣服於",
+"卧游" => "臥遊",
+"臧谷亡羊" => "臧穀亡羊",
+"自于" => "自於",
"自制" => "自製",
"自觉自愿" => "自覺自愿",
-"台上" => "臺上",
-"台下" => "臺下",
-"台中" => "臺中",
-"台北" => "臺北",
-"台南" => "臺南",
-"台地" => "臺地",
-"台塑" => "臺塑",
-"台大" => "臺大",
-"台币" => "臺幣",
-"台座" => "臺座",
-"台东" => "臺東",
-"台柱" => "臺柱",
-"台榭" => "臺榭",
-"台汽" => "臺汽",
-"台海" => "臺海",
-"台澎金马" => "臺澎金馬",
-"台湾" => "臺灣",
-"台灯" => "臺燈",
-"台球" => "臺球",
-"台省" => "臺省",
-"台端" => "臺端",
-"台糖" => "臺糖",
-"台肥" => "臺肥",
-"台航" => "臺航",
-"台视" => "臺視",
-"台词" => "臺詞",
-"台车" => "臺車",
-"台铁" => "臺鐵",
-"台阶" => "臺階",
-"台电" => "臺電",
-"台面" => "臺面",
+"自鸣钟" => "自鳴鐘",
+"至于" => "至於",
+"致力于" => "致力於",
+"致于" => "致於",
+"臻于" => "臻於",
+"臻于完善" => "臻於完善",
"舂谷" => "舂穀",
"兴致" => "興緻",
-"兴高采烈" => "興高采烈",
+"举手表" => "舉手表",
+"旧庄" => "舊庄",
"旧历" => "舊曆",
-"舒卷" => "舒卷",
-"舞台" => "舞臺",
+"旧药" => "舊藥",
+"舌干脣焦" => "舌乾脣焦",
+"舌后" => "舌後",
+"舒卷" => "舒捲",
"航海历" => "航海曆",
"船只" => "船隻",
"舰只" => "艦隻",
-"芬郁" => "芬郁",
-"花卷" => "花卷",
+"良药" => "良藥",
+"色欲" => "色慾",
+"艸木丰丰" => "艸木丰丰",
+"芍药" => "芍藥",
+"芒果干" => "芒果乾",
+"花拳绣腿" => "花拳繡腿",
+"花卷" => "花捲",
"花盆里" => "花盆裡",
-"花采" => "花采",
+"花药" => "花藥",
+"花哄" => "花鬨",
"苑里" => "苑裡",
-"若干" => "若干",
+"苛性硷" => "苛性鹼",
+"若于" => "若於",
"苦干" => "苦幹",
+"苦于" => "苦於",
+"苦药" => "苦藥",
"苦里" => "苦裏",
-"苦卤" => "苦鹵",
-"范仲淹" => "范仲淹",
-"范蠡" => "范蠡",
-"范阳" => "范陽",
-"茅台" => "茅臺",
+"苦斗" => "苦鬥",
+"苹萦" => "苹縈",
+"茵藉" => "茵藉",
"茶几" => "茶几",
+"茶庄" => "茶莊",
+"茶余" => "茶餘",
+"茶面" => "茶麵",
"草丛里" => "草叢裡",
+"草药" => "草藥",
+"荷花淀" => "荷花澱",
+"庄主" => "莊主",
+"庄周" => "莊周",
+"庄员" => "莊員",
+"庄严" => "莊嚴",
+"庄园" => "莊園",
+"庄子" => "莊子",
+"庄家" => "莊家",
+"庄户" => "莊戶",
+"庄敬" => "莊敬",
+"庄田" => "莊田",
+"庄稼" => "莊稼",
"庄里" => "莊裡",
+"庄重" => "莊重",
+"庄院" => "莊院",
"茎干" => "莖幹",
"莽荡" => "莽蕩",
-"菌丝体" => "菌絲体",
"菌丝体" => "菌絲體",
-"华里" => "華裡",
+"菜干" => "菜乾",
+"菠棱菜" => "菠稜菜",
+"菠萝干" => "菠蘿乾",
"华发" => "華髮",
-"万卷" => "萬卷",
+"菸硷" => "菸鹼",
+"万多只" => "萬多隻",
+"万天后" => "萬天後",
"万历" => "萬曆",
+"万签插架" => "萬籤插架",
+"万扎" => "萬紮",
+"万象" => "萬象",
"万只" => "萬隻",
+"万余" => "萬餘",
+"落后" => "落後",
+"落于" => "落於",
"落发" => "落髮",
"着儿" => "著兒",
"着书立说" => "著書立說",
@@ -3878,11 +5678,21 @@ $zh2Hant = array(
"着重指出" => "著重指出",
"着录" => "著錄",
"着录规则" => "著錄規則",
+"葡萄干" => "葡萄乾",
+"董氏封发" => "董氏封髮",
+"蒙汗药" => "蒙汗藥",
+"蒙雾露" => "蒙霧露",
+"蒜发" => "蒜髮",
+"苍术" => "蒼朮",
+"苍郁" => "蒼鬱",
"蓄发" => "蓄髮",
"蓄须" => "蓄鬚",
+"蓊郁" => "蓊鬱",
+"盖于" => "蓋於",
"蓬发" => "蓬髮",
"蓬松" => "蓬鬆",
-"莲台" => "蓮臺",
+"参绥" => "蔘綏",
+"荞麦面" => "蕎麥麵",
"荡来荡去" => "蕩來蕩去",
"荡女" => "蕩女",
"荡妇" => "蕩婦",
@@ -3894,30 +5704,124 @@ $zh2Hant = array(
"荡舟" => "蕩舟",
"荡船" => "蕩船",
"荡荡" => "蕩蕩",
-"薑丝" => "薑絲",
+"萧参" => "蕭蔘",
+"薄幸" => "薄倖",
+"薄干" => "薄幹",
+"姜是老的辣" => "薑是老的辣",
+"姜末" => "薑末",
+"姜桂" => "薑桂",
+"姜母" => "薑母",
+"姜汁" => "薑汁",
+"姜汤" => "薑湯",
+"姜片" => "薑片",
+"姜糖" => "薑糖",
+"姜丝" => "薑絲",
+"姜老辣" => "薑老辣",
+"姜蓉" => "薑蓉",
+"姜饼" => "薑餅",
+"姜黄" => "薑黃",
"薙发" => "薙髮",
-"借以" => "藉以",
-"借口" => "藉口",
-"借故" => "藉故",
-"借机" => "藉機",
-"借此" => "藉此",
-"借由" => "藉由",
-"借端" => "藉端",
-"借着" => "藉著",
-"借借" => "藉藉",
-"借词" => "藉詞",
-"借资" => "藉資",
-"借酒浇愁" => "藉酒澆愁",
+"薝卜" => "薝蔔",
+"藉以" => "藉以",
+"藉卉" => "藉卉",
+"藉寇兵" => "藉寇兵",
+"藉手" => "藉手",
+"藉槁" => "藉槁",
+"藉机" => "藉機",
+"藉此" => "藉此",
+"藉甚" => "藉甚",
+"藉由" => "藉由",
+"藉箸代筹" => "藉箸代籌",
+"藉草枕块" => "藉草枕塊",
+"藉着" => "藉著",
+"藉藉" => "藉藉",
+"藉资" => "藉資",
+"藏匿于" => "藏匿於",
+"藏于" => "藏於",
+"藏历" => "藏曆",
+"藏蒙歌儿" => "藏矇歌兒",
"藤制" => "藤製",
+"药丸" => "藥丸",
+"药典" => "藥典",
+"药到病除" => "藥到病除",
+"药剂" => "藥劑",
+"药力" => "藥力",
+"药包" => "藥包",
+"药名" => "藥名",
+"药味" => "藥味",
+"药品" => "藥品",
+"药商" => "藥商",
+"药单" => "藥單",
+"药婆" => "藥婆",
+"药学" => "藥學",
+"药害" => "藥害",
+"药专" => "藥專",
+"药局" => "藥局",
+"药师" => "藥師",
+"药店" => "藥店",
+"药厂" => "藥廠",
+"药引" => "藥引",
+"药性" => "藥性",
+"药房" => "藥房",
+"药效" => "藥效",
+"药方" => "藥方",
+"药材" => "藥材",
+"药棉" => "藥棉",
+"药水" => "藥水",
+"药油" => "藥油",
+"药液" => "藥液",
+"药渣" => "藥渣",
+"药片" => "藥片",
+"药物" => "藥物",
+"药王" => "藥王",
+"药理" => "藥理",
+"药瓶" => "藥瓶",
+"药用" => "藥用",
+"药皂" => "藥皂",
+"药盒" => "藥盒",
+"药石" => "藥石",
+"药科" => "藥科",
+"药箱" => "藥箱",
+"药签" => "藥籤",
+"药粉" => "藥粉",
+"药糖" => "藥糖",
+"药线" => "藥線",
+"药罐" => "藥罐",
+"药膏" => "藥膏",
+"药舖" => "藥舖",
+"药茶" => "藥茶",
+"药草" => "藥草",
+"药行" => "藥行",
+"药贩" => "藥販",
+"药费" => "藥費",
+"药酒" => "藥酒",
+"药量" => "藥量",
+"药针" => "藥針",
+"药铺" => "藥鋪",
+"药头" => "藥頭",
+"药饵" => "藥餌",
+"药面儿" => "藥麵兒",
+"苏昆" => "蘇崑",
"蕴含着" => "蘊含著",
"蕴涵着" => "蘊涵著",
-"蕴借" => "蘊藉",
+"蕴藉" => "蘊藉",
+"苹果干" => "蘋果乾",
"萝卜" => "蘿蔔",
+"萝卜干" => "蘿蔔乾",
"虎须" => "虎鬚",
+"处于" => "處於",
"号志" => "號誌",
-"蜂后" => "蜂后",
+"虫部" => "虫部",
+"蛇发女妖" => "蛇髮女妖",
+"蛔虫药" => "蛔蟲藥",
+"蜂涌" => "蜂湧",
+"蛏干" => "蟶乾",
"蛮干" => "蠻幹",
+"血余" => "血餘",
"行事历" => "行事曆",
+"行凶" => "行兇",
+"行凶后" => "行兇後",
+"行于" => "行於",
"胡同" => "衚衕",
"冲上" => "衝上",
"冲下" => "衝下",
@@ -3934,38 +5838,53 @@ $zh2Hant = array(
"冲口" => "衝口",
"冲垮" => "衝垮",
"冲堂" => "衝堂",
+"冲坚陷阵" => "衝堅陷陣",
"冲压" => "衝壓",
"冲天" => "衝天",
+"冲州撞府" => "衝州撞府",
+"冲心" => "衝心",
"冲掉" => "衝掉",
"冲撞" => "衝撞",
"冲击" => "衝擊",
"冲散" => "衝散",
+"冲杀" => "衝殺",
"冲决" => "衝決",
+"冲波" => "衝波",
"冲浪" => "衝浪",
"冲激" => "衝激",
+"冲然" => "衝然",
+"冲盹" => "衝盹",
"冲破" => "衝破",
"冲程" => "衝程",
"冲突" => "衝突",
"冲线" => "衝線",
"冲着" => "衝著",
-"冲冲" => "衝衝",
"冲要" => "衝要",
"冲起" => "衝起",
+"冲车" => "衝車",
"冲进" => "衝進",
"冲过" => "衝過",
+"冲量" => "衝量",
"冲锋" => "衝鋒",
+"冲陷" => "衝陷",
+"冲头阵" => "衝頭陣",
+"冲风" => "衝風",
+"表征" => "表徵",
"表里" => "表裡",
+"衷于" => "衷於",
"袖里" => "袖裡",
"被里" => "被裡",
"被复" => "被複",
-"被复" => "被覆",
"被复着" => "被覆著",
-"被发" => "被髮",
"裁并" => "裁併",
"裁制" => "裁製",
+"里勾外连" => "裏勾外連",
+"里手" => "裏手",
+"里海" => "裏海",
"里面" => "裏面",
-"里人" => "裡人",
-"里加" => "裡加",
+"补药" => "補藥",
+"补血药" => "補血藥",
+"补注" => "補註",
"里外" => "裡外",
"里子" => "裡子",
"里屋" => "裡屋",
@@ -3974,12 +5893,8 @@ $zh2Hant = array(
"里带" => "裡帶",
"里弦" => "裡弦",
"里应外合" => "裡應外合",
-"里拉" => "裡拉",
-"里斯" => "裡斯",
-"里海" => "裡海",
"里脊" => "裡脊",
"里衣" => "裡衣",
-"里里" => "裡裡",
"里通外国" => "裡通外國",
"里通外敌" => "裡通外敵",
"里边" => "裡邊",
@@ -4014,7 +5929,9 @@ $zh2Hant = array(
"复以百万" => "複以百萬",
"复位" => "複位",
"复信" => "複信",
+"复元音" => "複元音",
"复分数" => "複分數",
+"复分析" => "複分析",
"复列" => "複列",
"复利" => "複利",
"复印" => "複印",
@@ -4029,6 +5946,7 @@ $zh2Hant = array(
"复字键" => "複字鍵",
"复审" => "複審",
"复写" => "複寫",
+"复平面" => "複平面",
"复式" => "複式",
"复复" => "複復",
"复数" => "複數",
@@ -4039,7 +5957,7 @@ $zh2Hant = array(
"复次" => "複次",
"复比" => "複比",
"复决" => "複決",
-"复活" => "複活",
+"复流" => "複流",
"复测" => "複測",
"复亩珍" => "複畝珍",
"复发" => "複發",
@@ -4048,12 +5966,9 @@ $zh2Hant = array(
"复种" => "複種",
"复线" => "複線",
"复习" => "複習",
-"复兴社" => "複興社",
-"复旧" => "複舊",
"复色" => "複色",
"复叶" => "複葉",
"复盖" => "複蓋",
-"复苏" => "複蘇",
"复制" => "複製",
"复诊" => "複診",
"复词" => "複詞",
@@ -4062,6 +5977,7 @@ $zh2Hant = array(
"复议" => "複議",
"复变函数" => "複變函數",
"复赛" => "複賽",
+"复辅音" => "複輔音",
"复述" => "複述",
"复选" => "複選",
"复钱" => "複錢",
@@ -4071,106 +5987,216 @@ $zh2Hant = array(
"复韵" => "複韻",
"衬里" => "襯裡",
"西岳" => "西嶽",
-"西征" => "西征",
"西历" => "西曆",
+"西历史" => "西歷史",
+"西药" => "西藥",
+"西游" => "西遊",
"要冲" => "要衝",
"要么" => "要麼",
-"复上" => "覆上",
"复亡" => "覆亡",
-"复住" => "覆住",
-"复信" => "覆信",
"复命" => "覆命",
-"复在" => "覆在",
-"复审" => "覆審",
-"复巢之下" => "覆巢之下",
-"复成" => "覆成",
-"复败" => "覆敗",
-"复文" => "覆文",
-"复校" => "覆校",
-"复核" => "覆核",
+"复巢之下无完卵" => "覆巢之下無完卵",
"复水难收" => "覆水難收",
"复没" => "覆沒",
-"复灭" => "覆滅",
-"复盆" => "覆盆",
-"复舟" => "覆舟",
"复着" => "覆著",
"复盖" => "覆蓋",
"复盖着" => "覆蓋著",
-"复试" => "覆試",
-"复议" => "覆議",
-"复车" => "覆車",
-"复载" => "覆載",
"复辙" => "覆轍",
-"复电" => "覆電",
-"见复" => "見覆",
-"亲征" => "親征",
-"观众台" => "觀眾臺",
-"观台" => "觀臺",
-"观象台" => "觀象臺",
+"复雨翻云" => "覆雨翻雲",
+"见于" => "見於",
+"见棱见角" => "見稜見角",
+"见素抱朴" => "見素抱樸",
+"见钟不打" => "見鐘不打",
+"规划" => "規劃",
+"规范" => "規範",
+"视于" => "視於",
+"观采" => "觀採",
+"角落发" => "角落發",
"角落里" => "角落裡",
-"觔斗" => "觔斗",
+"觚棱" => "觚稜",
+"解雇" => "解僱",
+"解痛药" => "解痛藥",
+"解药" => "解藥",
+"解发佯狂" => "解髮佯狂",
"触须" => "觸鬚",
+"言大而夸" => "言大而夸",
+"言辩而确" => "言辯而确",
+"订于" => "訂於",
"订制" => "訂製",
+"计划" => "計劃",
+"讬了" => "託了",
+"讬事" => "託事",
+"讬交" => "託交",
+"讬人" => "託人",
+"讬付" => "託付",
+"讬古讽今" => "託古諷今",
+"讬名" => "託名",
+"讬咎" => "託咎",
+"讬梦" => "託夢",
+"讬大" => "託大",
+"讬孤" => "託孤",
+"讬故" => "託故",
+"讬疾" => "託疾",
+"讬病" => "託病",
+"讬福" => "託福",
+"讬管" => "託管",
+"讬言" => "託言",
+"讬词" => "託詞",
+"讬买" => "託買",
+"讬卖" => "託賣",
+"讬身" => "託身",
+"讬辞" => "託辭",
+"讬运" => "託運",
+"讬过" => "託過",
+"设于" => "設於",
+"许愿起经" => "許愿起經",
"诉说着" => "訴說著",
+"注上" => "註上",
+"注册" => "註冊",
+"注失" => "註失",
+"注定" => "註定",
+"注明" => "註明",
+"注标" => "註標",
+"注生娘娘" => "註生娘娘",
+"注疏" => "註疏",
+"注脚" => "註腳",
+"注解" => "註解",
+"注译" => "註譯",
+"注释" => "註釋",
+"注销" => "註銷",
+"评注" => "評註",
+"词干" => "詞幹",
"词汇" => "詞彙",
-"试卷" => "試卷",
-"诗卷" => "詩卷",
+"词余" => "詞餘",
+"询于" => "詢於",
+"诗云" => "詩云",
+"诗钟" => "詩鐘",
+"诗余" => "詩餘",
"话里有话" => "話裡有話",
+"该于" => "該於",
+"详注" => "詳註",
+"夸赞" => "誇讚",
"志哀" => "誌哀",
"志喜" => "誌喜",
"志庆" => "誌慶",
+"志异" => "誌異",
+"诱奸" => "誘姦",
"语云" => "語云",
"语汇" => "語彙",
+"诚征" => "誠徵",
+"诚朴" => "誠樸",
"诬蔑" => "誣衊",
-"诵经台" => "誦經臺",
"说着" => "說著",
-"课征" => "課征",
+"课后" => "課後",
+"课征" => "課徵",
+"课余" => "課餘",
+"调准" => "調準",
"调制" => "調製",
-"调频台" => "調頻臺",
"请参阅" => "請參閱",
-"讲台" => "講臺",
+"请讬" => "請託",
+"诸余" => "諸餘",
+"谋干" => "謀幹",
"谢绝参观" => "謝絕參觀",
+"谬采虚声" => "謬採虛聲",
+"謷丑" => "謷醜",
+"证于" => "證於",
+"警世钟" => "警世鐘",
+"警钟" => "警鐘",
+"译注" => "譯註",
"护发" => "護髮",
+"读后" => "讀後",
+"变丑" => "變醜",
"雠隙" => "讎隙",
-"豆腐干" => "豆腐干",
+"赞不绝口" => "讚不絕口",
+"赞佩" => "讚佩",
+"赞同" => "讚同",
+"赞叹" => "讚嘆",
+"赞扬" => "讚揚",
+"赞乐" => "讚樂",
+"赞歌" => "讚歌",
+"赞歎" => "讚歎",
+"赞美" => "讚美",
+"赞羨" => "讚羨",
+"赞许" => "讚許",
+"赞词" => "讚詞",
+"赞誉" => "讚譽",
+"赞赏" => "讚賞",
+"赞辞" => "讚辭",
+"赞颂" => "讚頌",
+"豆干" => "豆乾",
+"豆腐干" => "豆腐乾",
"竖着" => "豎著",
-"丰富多采" => "豐富多采",
"丰滨" => "豐濱",
"丰滨乡" => "豐濱鄉",
-"丰采" => "豐采",
+"象征" => "象徵",
"象征着" => "象徵著",
+"负债累累" => "負債纍纍",
+"贪欲" => "貪慾",
"贵干" => "貴幹",
-"贾后" => "賈后",
+"买凶" => "買兇",
+"贻范" => "貽範",
+"贾后" => "賈後",
"赈饥" => "賑饑",
-"贤后" => "賢后",
-"质朴" => "質朴",
+"质朴" => "質樸",
"赌台" => "賭檯",
+"赖于" => "賴於",
+"賸余" => "賸餘",
"购并" => "購併",
-"赤松" => "赤鬆",
-"起吊" => "起吊",
+"购买欲" => "購買慾",
+"赢余" => "贏餘",
+"走后" => "走後",
+"起因于" => "起因於",
"起复" => "起複",
+"起哄" => "起鬨",
"赶制" => "趕製",
+"赶面棍" => "趕麵棍",
+"赵庄" => "趙莊",
+"趋于" => "趨於",
+"趱干" => "趲幹",
+"足于" => "足於",
"跌荡" => "跌蕩",
-"跟斗" => "跟斗",
+"跟前跟后" => "跟前跟後",
+"路签" => "路籤",
"跳荡" => "跳蕩",
"跳表" => "跳錶",
-"踬仆" => "躓仆",
+"蹭棱子" => "蹭稜子",
+"躁郁" => "躁鬱",
+"躏藉" => "躪藉",
+"身后" => "身後",
+"身于" => "身於",
"躯干" => "軀幹",
"车库里" => "車庫裡",
"车站里" => "車站裡",
"车里" => "車裡",
+"轨范" => "軌範",
+"轩辟" => "軒闢",
+"载于" => "載於",
+"挽曲" => "輓曲",
+"挽歌" => "輓歌",
+"挽联" => "輓聯",
+"挽词" => "輓詞",
+"挽诗" => "輓詩",
+"轻于" => "輕於",
"轻松" => "輕鬆",
+"轮奸" => "輪姦",
"轮回" => "輪迴",
"转台" => "轉檯",
-"辛丑" => "辛丑",
-"辟邪" => "辟邪",
-"办伙" => "辦伙",
+"转讬" => "轉託",
+"辟谷" => "辟穀",
"办公台" => "辦公檯",
"辞汇" => "辭彙",
+"辫发" => "辮髮",
"农历" => "農曆",
+"农民历" => "農民曆",
+"农庄" => "農莊",
+"农药" => "農藥",
"迂回" => "迂迴",
+"近似于" => "近似於",
+"近于" => "近於",
"近日里" => "近日裡",
+"返朴" => "返樸",
"迥然回异" => "迥然迴異",
+"迫于" => "迫於",
"回光返照" => "迴光返照",
"回向" => "迴向",
"回圈" => "迴圈",
@@ -4192,155 +6218,385 @@ $zh2Hant = array(
"回避" => "迴避",
"回响" => "迴響",
"回风" => "迴風",
-"回首" => "迴首",
+"迷幻药" => "迷幻藥",
+"迷于" => "迷於",
"迷蒙" => "迷濛",
-"退伙" => "退伙",
+"迷药" => "迷藥",
+"迷魂药" => "迷魂藥",
+"追凶" => "追兇",
+"退后" => "退後",
+"退烧药" => "退燒藥",
+"逋发" => "逋髮",
+"透辟" => "透闢",
"这么着" => "這么著",
"这里" => "這裏",
"这里" => "這裡",
"这只" => "這隻",
"这么" => "這麼",
"这么着" => "這麼著",
+"通奸" => "通姦",
"通心面" => "通心麵",
+"通于" => "通於",
+"通历" => "通曆",
"速食面" => "速食麵",
+"连三并四" => "連三併四",
+"连占" => "連佔",
+"连采" => "連採",
+"连于" => "連於",
"连系" => "連繫",
-"连台好戏" => "連臺好戲",
+"连庄" => "連莊",
+"周游" => "週遊",
+"进占" => "進佔",
+"逼并" => "逼併",
+"游了" => "遊了",
+"游人" => "遊人",
+"游伴" => "遊伴",
+"游侠" => "遊俠",
+"游动" => "遊動",
+"游园" => "遊園",
+"游子" => "遊子",
+"游学" => "遊學",
+"游客" => "遊客",
+"游宦" => "遊宦",
+"游山玩水" => "遊山玩水",
+"游必有方" => "遊必有方",
+"游憩" => "遊憩",
+"游戏" => "遊戲",
+"游手好闲" => "遊手好閑",
+"游手好闲" => "遊手好閒",
+"游星" => "遊星",
+"游乐" => "遊樂",
+"游标卡尺" => "遊標卡尺",
+"游历" => "遊歷",
+"游民" => "遊民",
+"游牧" => "遊牧",
+"游猎" => "遊獵",
+"游玩" => "遊玩",
+"游荡" => "遊盪",
+"游丝" => "遊絲",
+"游兴" => "遊興",
+"游船" => "遊船",
+"游艇" => "遊艇",
"游荡" => "遊蕩",
-"遍布" => "遍佈",
+"游艺" => "遊藝",
+"游行" => "遊行",
+"游街" => "遊街",
+"游览" => "遊覽",
+"游记" => "遊記",
+"游说" => "遊說",
+"游资" => "遊資",
+"游走" => "遊走",
+"游踪" => "遊蹤",
+"游逛" => "遊逛",
+"游错" => "遊錯",
+"游离" => "遊離",
+"游骑兵" => "遊騎兵",
+"游魂" => "遊魂",
+"遍于" => "遍於",
+"过后" => "過後",
+"过于" => "過於",
+"过水面" => "過水麵",
+"道范" => "道範",
+"逊于" => "遜於",
"递回" => "遞迴",
-"远征" => "遠征",
-"适才" => "適纔",
-"遮复" => "遮覆",
+"远于" => "遠於",
+"远县才至" => "遠縣纔至",
+"远游" => "遠遊",
+"遨游" => "遨遊",
+"适于" => "適於",
+"遮丑" => "遮醜",
+"迁怒于" => "遷怒於",
+"迁于" => "遷於",
+"遗范" => "遺範",
+"遗余" => "遺餘",
+"辽沈" => "遼瀋",
+"避孕药" => "避孕藥",
+"还占" => "還佔",
+"还采" => "還採",
+"还政于民" => "還政於民",
+"还于" => "還於",
"还冲" => "還衝",
"邋里邋遢" => "邋裡邋遢",
"那里" => "那裡",
"那只" => "那隻",
"那么" => "那麼",
"那么着" => "那麼著",
-"邪辟" => "邪辟",
-"郁烈" => "郁烈",
-"郁穆" => "郁穆",
-"郁郁" => "郁郁",
-"郁闭" => "郁閉",
-"郁馥" => "郁馥",
+"郁朴" => "郁樸",
+"郊游" => "郊遊",
+"郘钟" => "郘鐘",
+"部落发" => "部落發",
+"都于" => "都於",
"乡愿" => "鄉愿",
-"乡里" => "鄉裡",
-"邻里" => "鄰裡",
+"郑凯云" => "鄭凱云",
"配合着" => "配合著",
+"配水干管" => "配水幹管",
+"配药" => "配藥",
"配制" => "配製",
+"酒后" => "酒後",
"酒杯" => "酒盃",
"酒坛" => "酒罈",
+"酒药" => "酒藥",
+"酒麴" => "酒麴",
"酥松" => "酥鬆",
+"酸硷" => "酸鹼",
+"醇朴" => "醇樸",
+"醉心于" => "醉心於",
+"醉于" => "醉於",
"醋坛" => "醋罈",
-"酝借" => "醞藉",
+"丑丫头" => "醜丫頭",
+"丑事" => "醜事",
+"丑人" => "醜人",
+"丑八怪" => "醜八怪",
+"丑剌剌" => "醜剌剌",
+"丑剧" => "醜劇",
+"丑化" => "醜化",
+"丑名" => "醜名",
+"丑咤" => "醜吒",
+"丑地" => "醜地",
+"丑夷" => "醜夷",
+"丑女效颦" => "醜女效顰",
+"丑妇" => "醜婦",
+"丑媳" => "醜媳",
+"丑小鸭" => "醜小鴨",
+"丑巴怪" => "醜巴怪",
+"丑恶" => "醜惡",
+"丑态" => "醜態",
+"丑于" => "醜於",
+"丑末" => "醜末",
+"丑样" => "醜樣",
+"丑死" => "醜死",
+"丑生" => "醜生",
+"丑闻" => "醜聞",
+"丑声四溢" => "醜聲四溢",
+"丑声远播" => "醜聲遠播",
+"丑脸" => "醜臉",
+"丑行" => "醜行",
+"丑诋" => "醜詆",
+"丑话" => "醜話",
+"丑语" => "醜語",
+"丑丑" => "醜醜",
+"丑陋" => "醜陋",
+"丑头怪脸" => "醜頭怪臉",
+"丑类" => "醜類",
+"酝藉" => "醞藉",
"酝酿着" => "醞釀著",
-"医药" => "醫葯",
-"醲郁" => "醲郁",
+"医药" => "醫藥",
"酿制" => "釀製",
-"采地" => "采地",
-"采女" => "采女",
-"采声" => "采聲",
-"采色" => "采色",
-"采邑" => "采邑",
+"衅钟" => "釁鐘",
+"采石之役" => "采石之役",
+"采石之战" => "采石之戰",
+"采石矶" => "采石磯",
+"釉药" => "釉藥",
"里程表" => "里程錶",
+"重划" => "重劃",
"重折" => "重摺",
+"重于" => "重於",
+"重罗面" => "重羅麵",
"重复" => "重複",
-"重复" => "重覆",
+"重讬" => "重託",
+"重游" => "重遊",
"重锤" => "重鎚",
-"野台戏" => "野臺戲",
-"金斗" => "金斗",
+"野姜" => "野薑",
+"野游" => "野遊",
+"厘出" => "釐出",
+"厘定" => "釐定",
+"厘正" => "釐正",
+"厘清" => "釐清",
+"厘订" => "釐訂",
+"金仆姑" => "金僕姑",
+"金仑溪" => "金崙溪",
"金表" => "金錶",
+"金钟" => "金鐘",
+"金鸡纳硷" => "金雞納鹼",
"金发" => "金髮",
+"金斗" => "金鬥",
"金霉素" => "金黴素",
"钉锤" => "釘鎚",
+"铃响后" => "鈴響後",
+"钩心斗角" => "鉤心鬥角",
"银朱" => "銀硃",
"银发" => "銀髮",
"铜制" => "銅製",
+"铜钟" => "銅鐘",
"铝制" => "鋁製",
+"钢梁" => "鋼樑",
"钢制" => "鋼製",
"录着" => "錄著",
"录制" => "錄製",
+"钱谷" => "錢穀",
+"钱庄" => "錢莊",
+"锦心绣口" => "錦心繡口",
+"锦绣花园" => "錦綉花園",
+"锦绣" => "錦繡",
"表带" => "錶帶",
"表店" => "錶店",
"表厂" => "錶廠",
+"表板" => "錶板",
"表壳" => "錶殼",
+"表盘" => "錶盤",
+"表蒙子" => "錶蒙子",
+"表针" => "錶針",
"表链" => "錶鏈",
-"表面" => "錶面",
-"锅台" => "鍋臺",
"锻鍊出" => "鍛鍊出",
-"锻鍊身体" => "鍛鍊身体",
"锲而不舍" => "鍥而不捨",
+"钟表" => "鍾錶",
"锤儿" => "鎚兒",
"锤子" => "鎚子",
"锤头" => "鎚頭",
"链霉素" => "鏈黴素",
-"镜台" => "鏡臺",
"锈病" => "鏽病",
"锈菌" => "鏽菌",
"锈蚀" => "鏽蝕",
+"钟不扣不鸣" => "鐘不扣不鳴",
+"钟不撞不鸣" => "鐘不撞不鳴",
+"钟乳洞" => "鐘乳洞",
+"钟乳石" => "鐘乳石",
+"钟在寺里" => "鐘在寺里",
+"钟塔" => "鐘塔",
+"钟山" => "鐘山",
+"钟形虫" => "鐘形蟲",
+"钟摆" => "鐘擺",
+"钟楼" => "鐘樓",
+"钟漏" => "鐘漏",
+"钟琴" => "鐘琴",
+"钟相" => "鐘相",
+"钟磬" => "鐘磬",
+"钟声" => "鐘聲",
+"钟表" => "鐘表",
"钟表" => "鐘錶",
+"钟关" => "鐘關",
+"钟响" => "鐘響",
+"钟头" => "鐘頭",
+"钟鸣" => "鐘鳴",
+"钟鸣漏尽" => "鐘鳴漏盡",
+"钟点" => "鐘點",
+"钟鼎" => "鐘鼎",
+"钟鼓" => "鐘鼓",
"铁锤" => "鐵鎚",
"铁锈" => "鐵鏽",
-"长征" => "長征",
-"长发" => "長髮",
+"铸钟" => "鑄鐘",
+"鑑于" => "鑑於",
+"鉴于" => "鑒於",
+"长于" => "長於",
+"长历" => "長曆",
+"长生药" => "長生藥",
"长须鲸" => "長鬚鯨",
+"门前门后" => "門前門後",
"门帘" => "門帘",
-"门斗" => "門斗",
"门里" => "門裡",
-"开伙" => "開伙",
-"开卷" => "開卷",
-"开诚布公" => "開誠佈公",
-"开采" => "開采",
-"閒情逸致" => "閒情逸緻",
-"閒荡" => "閒蕩",
+"门斗" => "門鬥",
+"开列于后" => "開列於後",
+"开吊" => "開弔",
+"开征" => "開徵",
+"开采" => "開採",
+"开药" => "開藥",
+"开辟" => "開闢",
+"开哄" => "開鬨",
+"闲情逸致" => "閒情逸緻",
+"闲荡" => "閒蕩",
+"闲游" => "閒遊",
"间不容发" => "間不容髮",
-"闵采尔" => "閔采爾",
-"阅卷" => "閱卷",
-"阑干" => "闌干",
+"闵采尔" => "閔採爾",
+"合府" => "閤府",
+"闺范" => "閨範",
+"阃范" => "閫範",
"关系" => "關係",
"关系着" => "關係著",
+"关弓与我确" => "關弓與我确",
+"关于" => "關於",
+"辟佛" => "闢佛",
+"辟作" => "闢作",
+"辟划" => "闢劃",
+"辟土" => "闢土",
+"辟地" => "闢地",
+"辟室" => "闢室",
+"辟建" => "闢建",
+"辟为" => "闢為",
+"辟田" => "闢田",
+"辟筑" => "闢築",
+"辟谣" => "闢謠",
+"辟辟" => "闢辟",
+"辟邪以律" => "闢邪以律",
"防御" => "防禦",
+"防范" => "防範",
"防锈" => "防鏽",
"防台" => "防颱",
-"阿斗" => "阿斗",
-"阿里" => "阿裡",
-"除旧布新" => "除舊佈新",
-"阴干" => "陰干",
+"阻于" => "阻於",
+"附于" => "附於",
+"附注" => "附註",
+"降压药" => "降壓藥",
+"降于" => "降於",
+"限于" => "限於",
+"升官" => "陞官",
+"除臭药" => "除臭藥",
+"阴干" => "陰乾",
"阴历" => "陰曆",
-"阴郁" => "陰郁",
-"陆征祥" => "陸征祥",
+"阴郁" => "陰鬱",
+"陷于" => "陷於",
+"陆游" => "陸遊",
"阳春面" => "陽春麵",
"阳历" => "陽曆",
-"阳台" => "陽臺",
+"随后" => "隨後",
+"随于" => "隨於",
+"隐几" => "隱几",
+"隐于" => "隱於",
"只字" => "隻字",
"只影" => "隻影",
"只手遮天" => "隻手遮天",
"只眼" => "隻眼",
"只言片语" => "隻言片語",
"只身" => "隻身",
+"雅范" => "雅範",
"雅致" => "雅緻",
-"雇佣" => "雇佣",
+"集于" => "集於",
+"集于一身" => "集於一身",
+"集游法" => "集遊法",
+"雇佣" => "雇傭",
+"雇于" => "雇於",
+"雕梁划栋" => "雕樑畫棟",
"双折" => "雙摺",
+"双胜类" => "雙胜類",
+"杂合面儿" => "雜合麵兒",
"杂志" => "雜誌",
+"杂面" => "雜麵",
+"鸡奸" => "雞姦",
"鸡丝" => "雞絲",
"鸡丝面" => "雞絲麵",
"鸡腿面" => "雞腿麵",
"鸡只" => "雞隻",
+"离于" => "離於",
+"难容于" => "難容於",
"难舍" => "難捨",
+"难于" => "難於",
+"雨后" => "雨後",
+"雪窗萤几" => "雪窗螢几",
"雪里" => "雪裡",
+"云南白药" => "雲南白藥",
+"云笈七签" => "雲笈七籤",
+"云游" => "雲遊",
"云须" => "雲鬚",
+"零多只" => "零多隻",
+"零天后" => "零天後",
+"零只" => "零隻",
"电子表" => "電子錶",
-"电台" => "電臺",
+"电子钟" => "電子鐘",
"电冲" => "電衝",
-"电复" => "電覆",
-"电视台" => "電視臺",
"电表" => "電錶",
+"电钟" => "電鐘",
+"震栗" => "震慄",
+"震于" => "震於",
"震荡" => "震蕩",
"雾里" => "霧裡",
-"露台" => "露臺",
-"灵台" => "靈臺",
-"青瓦台" => "青瓦臺",
+"露丑" => "露醜",
+"霸占" => "霸佔",
+"霁范" => "霽範",
+"灵药" => "靈藥",
+"青山一发" => "青山一髮",
+"青苹" => "青苹",
"青霉" => "青黴",
+"非占不可" => "非佔不可",
+"非于" => "非於",
+"靠后" => "靠後",
"面朝着" => "面朝著",
"面临着" => "面臨著",
"鞋里" => "鞋裡",
@@ -4349,20 +6605,33 @@ $zh2Hant = array(
"鞭辟入里" => "鞭辟入裡",
"韩国制" => "韓國製",
"韩制" => "韓製",
+"音准" => "音準",
+"音声如钟" => "音聲如鐘",
+"韶山冲" => "韶山衝",
+"顺于" => "順於",
+"颂赞" => "頌讚",
"预制" => "預製",
-"颁布" => "頒佈",
+"领袖欲" => "領袖慾",
"头里" => "頭裡",
"头发" => "頭髮",
"颊须" => "頰鬚",
-"颠仆" => "顛仆",
-"颠复" => "顛複",
+"额我略历" => "額我略曆",
+"颜范" => "顏範",
+"颠干倒坤" => "顛乾倒坤",
"颠复" => "顛覆",
+"类似于" => "類似於",
+"顾藉" => "顧藉",
+"颤栗" => "顫慄",
"显着标志" => "顯著標志",
+"风干" => "風乾",
"风土志" => "風土誌",
-"风斗" => "風斗",
+"风尘仆仆" => "風塵僕僕",
+"风卷残云" => "風捲殘雲",
"风物志" => "風物誌",
+"风范" => "風範",
"风里" => "風裡",
-"风采" => "風采",
+"风起云涌" => "風起雲湧",
+"风斗" => "風鬥",
"台风" => "颱風",
"刮了" => "颳了",
"刮倒" => "颳倒",
@@ -4371,96 +6640,233 @@ $zh2Hant = array(
"刮着" => "颳著",
"刮走" => "颳走",
"刮起" => "颳起",
+"刮雪" => "颳雪",
"刮风" => "颳風",
"飘荡" => "飄蕩",
+"飘游" => "飄遊",
+"食欲" => "食慾",
+"食野之苹" => "食野之苹",
+"食面" => "食麵",
+"饭后" => "飯後",
+"饭后钟" => "飯後鐘",
"饭团" => "飯糰",
-"饼干" => "餅干",
+"饭庄" => "飯莊",
+"饲喂" => "飼餵",
+"饼干" => "餅乾",
+"馂余" => "餕餘",
+"余下" => "餘下",
+"余事" => "餘事",
+"余人" => "餘人",
+"余值" => "餘值",
+"余僇" => "餘僇",
+"余光" => "餘光",
+"余函数" => "餘函數",
+"余刃" => "餘刃",
+"余切" => "餘切",
+"余利" => "餘利",
+"余剩" => "餘剩",
+"余割" => "餘割",
+"余力" => "餘力",
+"余勇" => "餘勇",
+"余味" => "餘味",
+"余喘" => "餘喘",
+"余地" => "餘地",
+"余址" => "餘址",
+"余墨" => "餘墨",
+"余外" => "餘外",
+"余妙" => "餘妙",
+"余姚" => "餘姚",
+"余威" => "餘威",
+"余存" => "餘存",
+"余孽" => "餘孽",
+"余巾" => "餘巾",
+"余式定理" => "餘式定理",
+"余弦" => "餘弦",
+"余思" => "餘思",
+"余悸" => "餘悸",
+"余庆" => "餘慶",
+"余数" => "餘數",
+"余日" => "餘日",
+"余明" => "餘明",
+"余映" => "餘映",
+"余暇" => "餘暇",
+"余晖" => "餘暉",
+"余杭" => "餘杭",
+"余杯" => "餘杯",
+"余桃" => "餘桃",
+"余桶" => "餘桶",
+"余业" => "餘業",
+"余款" => "餘款",
+"余步" => "餘步",
+"余殃" => "餘殃",
+"余毒" => "餘毒",
+"余气" => "餘氣",
+"余氯" => "餘氯",
+"余波" => "餘波",
+"余温" => "餘溫",
+"余泽" => "餘澤",
+"余沥" => "餘瀝",
+"余烈" => "餘烈",
+"余热" => "餘熱",
+"余烬" => "餘燼",
+"余珍" => "餘珍",
+"余生" => "餘生",
+"余留" => "餘留",
+"余众" => "餘眾",
+"余窍" => "餘竅",
+"余粮" => "餘糧",
+"余绪" => "餘緒",
+"余缺" => "餘缺",
+"余罪" => "餘罪",
+"余羨" => "餘羨",
+"余声" => "餘聲",
+"余膏" => "餘膏",
+"余兴" => "餘興",
+"余蓄" => "餘蓄",
+"余荫" => "餘蔭",
+"余裕" => "餘裕",
+"余角" => "餘角",
+"余论" => "餘論",
+"余责" => "餘責",
+"余辉" => "餘輝",
+"余辜" => "餘辜",
+"余酲" => "餘酲",
+"余钱" => "餘錢",
+"余闰" => "餘閏",
+"余闲" => "餘閒",
+"余震" => "餘震",
+"余音" => "餘音",
+"余韵" => "餘韻",
+"余响" => "餘響",
+"余额" => "餘額",
+"余风" => "餘風",
+"余食" => "餘食",
+"余香" => "餘香",
+"余党" => "餘黨",
"馄饨面" => "餛飩麵",
-"饥不择食" => "饑不擇食",
+"馆谷" => "館穀",
+"喂乳" => "餵乳",
+"喂了" => "餵了",
+"喂奶" => "餵奶",
+"喂给" => "餵給",
+"喂羊" => "餵羊",
+"喂猪" => "餵豬",
+"喂过" => "餵過",
+"喂鸡" => "餵雞",
+"喂食" => "餵食",
+"喂饱" => "餵飽",
+"喂养" => "餵養",
+"喂驴" => "餵驢",
+"喂鱼" => "餵魚",
+"喂鸭" => "餵鴨",
+"喂鹅" => "餵鵝",
"饥寒" => "饑寒",
"饥民" => "饑民",
"饥渴" => "饑渴",
"饥溺" => "饑溺",
-"饥荒" => "饑荒",
"饥饱" => "饑飽",
-"饥饿" => "饑餓",
"饥馑" => "饑饉",
"首当其冲" => "首當其衝",
-"香郁" => "香郁",
-"馥郁" => "馥郁",
-"马里" => "馬裡",
+"香干" => "香乾",
+"马干" => "馬乾",
+"马后" => "馬後",
"马表" => "馬錶",
+"驻扎" => "駐紮",
"骀荡" => "駘蕩",
+"骀藉" => "駘藉",
"腾冲" => "騰衝",
+"惊赞" => "驚讚",
"骨子里" => "骨子裡",
"骨干" => "骨幹",
"骨灰坛" => "骨灰罈",
+"骨坛" => "骨罈",
+"骨头里挣出来的钱才做得肉" => "骨頭裡掙出來的錢纔做得肉",
"肮脏" => "骯髒",
"脏乱" => "髒亂",
"脏兮兮" => "髒兮兮",
"脏字" => "髒字",
"脏得" => "髒得",
+"脏心" => "髒心",
"脏东西" => "髒東西",
"脏水" => "髒水",
"脏的" => "髒的",
+"脏词" => "髒詞",
"脏话" => "髒話",
"脏钱" => "髒錢",
+"体范" => "體範",
+"高几" => "高几",
"高干" => "高幹",
-"高台" => "高臺",
+"高于" => "高於",
+"高升" => "高陞",
+"髡发" => "髡髮",
"髭须" => "髭鬚",
+"发上指冠" => "髮上指冠",
+"发上冲冠" => "髮上沖冠",
+"发乳" => "髮乳",
+"发光可鉴" => "髮光可鑒",
+"发匪" => "髮匪",
"发型" => "髮型",
"发夹" => "髮夾",
"发妻" => "髮妻",
"发姐" => "髮姐",
+"发屋" => "髮屋",
"发带" => "髮帶",
"发廊" => "髮廊",
"发式" => "髮式",
+"发引千钧" => "髮引千鈞",
"发指" => "髮指",
-"发捲" => "髮捲",
+"发卷" => "髮捲",
"发根" => "髮根",
-"发毛" => "髮毛",
"发油" => "髮油",
+"发漂" => "髮漂",
"发状" => "髮狀",
+"发癣" => "髮癬",
"发短心长" => "髮短心長",
-"发端" => "髮端",
+"发禁" => "髮禁",
+"发笺" => "髮箋",
+"发纱" => "髮紗",
"发结" => "髮結",
"发丝" => "髮絲",
"发网" => "髮網",
+"发脚" => "髮腳",
"发肤" => "髮膚",
"发胶" => "髮膠",
"发菜" => "髮菜",
"发蜡" => "髮蠟",
+"发踊冲冠" => "髮踴沖冠",
"发辫" => "髮辮",
"发针" => "髮針",
+"发钗" => "髮釵",
"发长" => "髮長",
"发际" => "髮際",
"发霜" => "髮霜",
+"发饰" => "髮飾",
"发髻" => "髮髻",
"发鬓" => "髮鬢",
+"髼松" => "髼鬆",
"鬅松" => "鬅鬆",
+"松一口气" => "鬆一口氣",
"松了" => "鬆了",
"松些" => "鬆些",
"松劲" => "鬆勁",
"松动" => "鬆動",
"松口" => "鬆口",
"松土" => "鬆土",
+"松宽" => "鬆寬",
"松弛" => "鬆弛",
"松快" => "鬆快",
"松懈" => "鬆懈",
"松手" => "鬆手",
"松散" => "鬆散",
-"松林" => "鬆林",
"松柔" => "鬆柔",
-"松毛虫" => "鬆毛蟲",
+"松气" => "鬆氣",
"松浮" => "鬆浮",
-"松涛" => "鬆濤",
-"松科" => "鬆科",
-"松节油" => "鬆節油",
"松绑" => "鬆綁",
"松紧" => "鬆緊",
"松缓" => "鬆緩",
"松脆" => "鬆脆",
"松脱" => "鬆脫",
+"松蛋" => "鬆蛋",
"松起" => "鬆起",
"松软" => "鬆軟",
"松通" => "鬆通",
@@ -4473,111 +6879,278 @@ $zh2Hant = array(
"胡渣" => "鬍渣",
"胡髭" => "鬍髭",
"胡须" => "鬍鬚",
+"鬒发" => "鬒髮",
"须根" => "鬚根",
"须毛" => "鬚毛",
"须生" => "鬚生",
"须眉" => "鬚眉",
"须发" => "鬚髮",
"须须" => "鬚鬚",
+"须鲨" => "鬚鯊",
+"须鲸" => "鬚鯨",
"鬓发" => "鬢髮",
+"斗上" => "鬥上",
+"斗不过" => "鬥不過",
+"斗了" => "鬥了",
+"斗来斗去" => "鬥來鬥去",
+"斗倒" => "鬥倒",
+"斗劲" => "鬥勁",
+"斗口" => "鬥口",
+"斗嘴" => "鬥嘴",
+"斗士" => "鬥士",
+"斗子" => "鬥子",
+"斗弄" => "鬥弄",
+"斗志" => "鬥志",
+"斗成" => "鬥成",
+"斗批改" => "鬥批改",
+"斗技" => "鬥技",
+"斗智" => "鬥智",
+"斗殴" => "鬥毆",
+"斗气" => "鬥氣",
+"斗法" => "鬥法",
+"斗争" => "鬥爭",
+"斗牛" => "鬥牛",
+"斗狠" => "鬥狠",
+"斗眼" => "鬥眼",
+"斗私批修" => "鬥私批修",
+"斗笠" => "鬥笠",
+"斗草" => "鬥草",
"斗着" => "鬥著",
+"斗蟋蟀" => "鬥蟋蟀",
+"斗起" => "鬥起",
+"斗鸡" => "鬥雞",
+"斗斗" => "鬥鬥",
+"斗鱼" => "鬥魚",
+"斗鹌鹑" => "鬥鵪鶉",
"闹着玩儿" => "鬧著玩儿",
"闹着玩儿" => "鬧著玩兒",
+"闹钟" => "鬧鐘",
+"哄动" => "鬨動",
+"哄堂" => "鬨堂",
+"哄笑" => "鬨笑",
+"郁伊" => "鬱伊",
+"郁勃" => "鬱勃",
+"郁卒" => "鬱卒",
+"郁堙不偶" => "鬱堙不偶",
+"郁塞" => "鬱塞",
+"郁垒" => "鬱壘",
+"郁律" => "鬱律",
+"郁悒" => "鬱悒",
+"郁闷" => "鬱悶",
+"郁愤" => "鬱憤",
+"郁抑" => "鬱抑",
+"郁挹" => "鬱挹",
+"郁江" => "鬱江",
+"郁沉沉" => "鬱沉沉",
+"郁泱" => "鬱泱",
+"郁火" => "鬱火",
+"郁热" => "鬱熱",
+"郁燠" => "鬱燠",
+"郁症" => "鬱症",
+"郁积" => "鬱積",
+"郁纡" => "鬱紆",
+"郁结" => "鬱結",
+"郁蒸" => "鬱蒸",
+"郁蓊" => "鬱蓊",
+"郁血" => "鬱血",
+"郁邑" => "鬱邑",
"郁郁" => "鬱郁",
+"郁金" => "鬱金",
+"郁金香" => "鬱金香",
+"郁闭" => "鬱閉",
+"郁陶" => "鬱陶",
+"郁郁不平" => "鬱鬱不平",
+"郁郁不乐" => "鬱鬱不樂",
+"郁郁寡欢" => "鬱鬱寡歡",
+"郁郁而终" => "鬱鬱而終",
+"郁郁葱葱" => "鬱鬱蔥蔥",
+"郁黑" => "鬱黑",
+"魏征" => "魏徵",
+"鱼干" => "魚乾",
"鱼松" => "魚鬆",
"鲸须" => "鯨鬚",
"鲇鱼" => "鯰魚",
+"鸠占鹊巢" => "鳩佔鵲巢",
+"凤梨干" => "鳳梨乾",
+"鸣钟" => "鳴鐘",
+"鸿范" => "鴻範",
+"鹄发" => "鵠髮",
+"雕悍" => "鵰悍",
"鹤发" => "鶴髮",
-"卤化" => "鹵化",
-"卤味" => "鹵味",
-"卤族" => "鹵族",
-"卤水" => "鹵水",
-"卤汁" => "鹵汁",
-"卤簿" => "鹵簿",
-"卤素" => "鹵素",
-"卤莽" => "鹵莽",
-"卤钝" => "鹵鈍",
"咸味" => "鹹味",
+"咸嘴淡舌" => "鹹嘴淡舌",
"咸土" => "鹹土",
"咸度" => "鹹度",
"咸得" => "鹹得",
+"咸批" => "鹹批",
"咸水" => "鹹水",
+"咸派" => "鹹派",
"咸海" => "鹹海",
"咸淡" => "鹹淡",
"咸湖" => "鹹湖",
"咸汤" => "鹹湯",
+"咸潟" => "鹹潟",
"咸的" => "鹹的",
+"咸粥" => "鹹粥",
"咸肉" => "鹹肉",
"咸菜" => "鹹菜",
"咸蛋" => "鹹蛋",
"咸猪肉" => "鹹豬肉",
"咸类" => "鹹類",
+"咸食" => "鹹食",
"咸鱼" => "鹹魚",
"咸鸭蛋" => "鹹鴨蛋",
"咸卤" => "鹹鹵",
"咸咸" => "鹹鹹",
-"盐卤" => "鹽鹵",
+"硷化" => "鹼化",
+"硷土金属" => "鹼土金屬",
+"硷地" => "鹼地",
+"硷度" => "鹼度",
+"硷性" => "鹼性",
+"硷水" => "鹼水",
+"硷液" => "鹼液",
+"硷熔" => "鹼熔",
+"硷石灰" => "鹼石灰",
+"硷纤维素" => "鹼纖維素",
+"硷金属" => "鹼金屬",
+"硷类" => "鹼類",
+"盐打怎么咸" => "鹽打怎麼鹹",
+"盐卤" => "鹽滷",
+"盐余" => "鹽餘",
+"盐硷土" => "鹽鹼土",
+"盐硷滩" => "鹽鹼灘",
+"丽于" => "麗於",
+"麴霉" => "麴黴",
+"面人儿" => "麵人兒",
"面价" => "麵價",
"面包" => "麵包",
"面团" => "麵團",
+"面坊" => "麵坊",
+"面坯儿" => "麵坯兒",
+"面塑" => "麵塑",
"面店" => "麵店",
"面厂" => "麵廠",
"面杖" => "麵杖",
"面条" => "麵條",
+"面汤" => "麵湯",
+"面浆" => "麵漿",
"面灰" => "麵灰",
+"面疙瘩" => "麵疙瘩",
"面皮" => "麵皮",
+"面码儿" => "麵碼兒",
"面筋" => "麵筋",
"面粉" => "麵粉",
"面糊" => "麵糊",
"面线" => "麵線",
+"面缸" => "麵缸",
"面茶" => "麵茶",
"面食" => "麵食",
"面饺" => "麵餃",
"面饼" => "麵餅",
+"面馆" => "麵館",
+"麻药" => "麻藥",
+"麻醉药" => "麻醉藥",
"麻酱面" => "麻醬麵",
+"麻雀在后" => "麻雀在後",
+"黄干黑廋" => "黃乾黑廋",
"黄历" => "黃曆",
+"黄钟" => "黃鐘",
+"黄雀在后" => "黃雀在後",
"黄发垂髫" => "黃髮垂髫",
+"黑奴吁天录" => "黑奴籲天錄",
"黑发" => "黑髮",
-"黑松" => "黑鬆",
+"点钟" => "點鐘",
"霉毒" => "黴毒",
"霉菌" => "黴菌",
+"霉黑" => "黴黑",
+"霉黧" => "黴黧",
"鼓里" => "鼓裡",
"冬冬" => "鼕鼕",
-"龙卷" => "龍卷",
+"鼠药" => "鼠藥",
+"鼻梁" => "鼻樑",
+"鼻准" => "鼻準",
+"齐王舍牛" => "齊王捨牛",
+"齿危发秀" => "齒危髮秀",
+"齿发" => "齒髮",
+"出剧" => "齣劇",
+"出卡通" => "齣卡通",
+"出戏" => "齣戲",
+"出电影" => "齣電影",
+"龙卷" => "龍捲",
+"龙争虎斗" => "龍爭虎鬥",
+"龙眼干" => "龍眼乾",
+"龙虎斗" => "龍虎鬥",
"龙须" => "龍鬚",
+"龟山庄" => "龜山庄",
+"手塚治虫" => "手塚治虫",
+"校仇" => "校讎",
+"仇校" => "讎校",
+"仇夷" => "讎夷",
+"仇問" => "讎問",
+"無言不仇" => "無言不讎",
+"視如寇仇" => "視如寇讎",
+"往日無仇" => "往日無讎",
+"近日無仇" => "近日無讎",
+"李連杰" => "李連杰",
+"周杰倫" => "周杰倫",
+"寶曆" => "寶曆",
+"涂謹申" => "涂謹申",
+"於姓" => "於姓",
+"於氏" => "於氏",
+"於夫羅" => "於夫羅",
+"於梨華" => "於梨華",
+"鄭凱云" => "鄭凱云",
+"筑陽" => "筑陽",
+"筑後" => "筑後",
+"采石磯" => "采石磯",
+"采石之戰" => "采石之戰",
+"張三丰" => "張三丰",
+"丰韻" => "丰韻",
+"丰儀" => "丰儀",
+"丰標不凡" => "丰標不凡",
+"干細胞" => "幹細胞",
+"干熱" => "乾熱",
+"二里頭" => "二里頭",
+"水里鄉" => "水里鄉",
+"蒙胧" => "朦朧",
+"酒曲" => "酒麴",
+"呆里呆气" => "呆裡呆氣",
+"拜托" => "拜託",
+"委托书" => "委託書",
+"委托" => "委託",
+"挽詞" => "輓詞",
+"挽聯" => "輓聯",
+"挽詩" => "輓詩",
+"於夫罗" => "於夫羅",
+"府干預" => "府干預",
+"府干擾" => "府干擾",
);
$zh2Hans = array(
+"餘"=>"余",
"瀋"=>"沈",
"畫"=>"划",
"鍾"=>"钟",
"靦"=>"腼",
-"餘"=>"余",
"鯰"=>"鲇",
"鹼"=>"硷",
"㠏"=>"㟆",
"𡞵"=>"㛟",
"万"=>"万",
"与"=>"与",
-"丑"=>"丑",
"丟"=>"丢",
"並"=>"并",
"丰"=>"丰",
-"么"=>"么",
"乾"=>"干",
"亂"=>"乱",
"云"=>"云",
"亙"=>"亘",
"亞"=>"亚",
-"仆"=>"仆",
"价"=>"价",
-"伙"=>"伙",
"佇"=>"伫",
"佈"=>"布",
"体"=>"体",
-"余"=>"余",
-"余"=>"馀",
-"佣"=>"佣",
+"佔"=>"占",
"併"=>"并",
"來"=>"来",
"侖"=>"仑",
@@ -4592,6 +7165,7 @@ $zh2Hans = array(
"倉"=>"仓",
"個"=>"个",
"們"=>"们",
+"倖"=>"幸",
"倫"=>"伦",
"偉"=>"伟",
"側"=>"侧",
@@ -4617,6 +7191,7 @@ $zh2Hans = array(
"僞"=>"伪",
"僥"=>"侥",
"僨"=>"偾",
+"僱"=>"雇",
"價"=>"价",
"儀"=>"仪",
"儂"=>"侬",
@@ -4645,13 +7220,11 @@ $zh2Hans = array(
"兩"=>"两",
"冊"=>"册",
"冪"=>"幂",
-"准"=>"准",
"凈"=>"净",
"凍"=>"冻",
"凜"=>"凛",
"几"=>"几",
"凱"=>"凯",
-"划"=>"划",
"別"=>"别",
"刪"=>"删",
"剄"=>"刭",
@@ -4690,11 +7263,11 @@ $zh2Hans = array(
"匱"=>"匮",
"區"=>"区",
"協"=>"协",
-"卷"=>"卷",
"卻"=>"却",
"厂"=>"厂",
"厙"=>"厍",
"厠"=>"厕",
+"厤"=>"历",
"厭"=>"厌",
"厲"=>"厉",
"厴"=>"厣",
@@ -4703,13 +7276,11 @@ $zh2Hans = array(
"叢"=>"丛",
"台"=>"台",
"叶"=>"叶",
-"吊"=>"吊",
"后"=>"后",
"吒"=>"咤",
"吳"=>"吴",
"吶"=>"呐",
"呂"=>"吕",
-"呆"=>"獃",
"咼"=>"呙",
"員"=>"员",
"唄"=>"呗",
@@ -4831,6 +7402,7 @@ $zh2Hans = array(
"壽"=>"寿",
"夠"=>"够",
"夢"=>"梦",
+"夥"=>"伙",
"夾"=>"夹",
"奐"=>"奂",
"奧"=>"奥",
@@ -4841,7 +7413,6 @@ $zh2Hans = array(
"奼"=>"姹",
"妝"=>"妆",
"姍"=>"姗",
-"姜"=>"姜",
"姦"=>"奸",
"娛"=>"娱",
"婁"=>"娄",
@@ -4871,6 +7442,7 @@ $zh2Hans = array(
"孿"=>"孪",
"宁"=>"宁",
"宮"=>"宫",
+"寀"=>"采",
"寢"=>"寝",
"實"=>"实",
"寧"=>"宁",
@@ -4898,7 +7470,9 @@ $zh2Hans = array(
"島"=>"岛",
"峽"=>"峡",
"崍"=>"崃",
+"崑"=>"昆",
"崗"=>"岗",
+"崙"=>"仑",
"崢"=>"峥",
"崬"=>"岽",
"嵐"=>"岚",
@@ -4953,6 +7527,7 @@ $zh2Hans = array(
"廬"=>"庐",
"廳"=>"厅",
"弒"=>"弑",
+"弔"=>"吊",
"弳"=>"弪",
"張"=>"张",
"強"=>"强",
@@ -4963,7 +7538,6 @@ $zh2Hans = array(
"彙"=>"汇",
"彞"=>"彝",
"彥"=>"彦",
-"征"=>"征",
"後"=>"后",
"徑"=>"径",
"從"=>"从",
@@ -5002,6 +7576,7 @@ $zh2Hans = array(
"慮"=>"虑",
"慳"=>"悭",
"慶"=>"庆",
+"慾"=>"欲",
"憂"=>"忧",
"憊"=>"惫",
"憐"=>"怜",
@@ -5039,11 +7614,13 @@ $zh2Hans = array(
"戶"=>"户",
"担"=>"担",
"拋"=>"抛",
+"拚"=>"拼",
"挩"=>"捝",
"挾"=>"挟",
"捨"=>"舍",
"捫"=>"扪",
"据"=>"据",
+"捲"=>"卷",
"掃"=>"扫",
"掄"=>"抡",
"掗"=>"挜",
@@ -5122,10 +7699,10 @@ $zh2Hans = array(
"斂"=>"敛",
"斃"=>"毙",
"斕"=>"斓",
-"斗"=>"斗",
"斬"=>"斩",
"斷"=>"断",
"於"=>"于",
+"昇"=>"升",
"時"=>"时",
"晉"=>"晋",
"晝"=>"昼",
@@ -5147,8 +7724,6 @@ $zh2Hans = array(
"會"=>"会",
"朧"=>"胧",
"朮"=>"术",
-"术"=>"术",
-"朴"=>"朴",
"東"=>"东",
"杴"=>"锨",
"极"=>"极",
@@ -5173,6 +7748,7 @@ $zh2Hans = array(
"楨"=>"桢",
"業"=>"业",
"極"=>"极",
+"榦"=>"干",
"榪"=>"杩",
"榮"=>"荣",
"榲"=>"榅",
@@ -5186,6 +7762,7 @@ $zh2Hans = array(
"樁"=>"桩",
"樂"=>"乐",
"樅"=>"枞",
+"樑"=>"梁",
"樓"=>"楼",
"標"=>"标",
"樞"=>"枢",
@@ -5228,6 +7805,7 @@ $zh2Hans = array(
"櫸"=>"榉",
"櫻"=>"樱",
"欄"=>"栏",
+"欅"=>"榉",
"權"=>"权",
"欏"=>"椤",
"欒"=>"栾",
@@ -5273,7 +7851,6 @@ $zh2Hans = array(
"涂"=>"涂",
"涇"=>"泾",
"涼"=>"凉",
-"淀"=>"淀",
"淒"=>"凄",
"淚"=>"泪",
"淥"=>"渌",
@@ -5290,6 +7867,7 @@ $zh2Hans = array(
"渾"=>"浑",
"湊"=>"凑",
"湞"=>"浈",
+"湧"=>"涌",
"湯"=>"汤",
"溈"=>"沩",
"準"=>"准",
@@ -5299,6 +7877,7 @@ $zh2Hans = array(
"滅"=>"灭",
"滌"=>"涤",
"滎"=>"荥",
+"滙"=>"汇",
"滬"=>"沪",
"滯"=>"滞",
"滲"=>"渗",
@@ -5404,6 +7983,7 @@ $zh2Hans = array(
"燜"=>"焖",
"營"=>"营",
"燦"=>"灿",
+"燬"=>"毁",
"燭"=>"烛",
"燴"=>"烩",
"燶"=>"㶶",
@@ -5470,6 +8050,7 @@ $zh2Hans = array(
"甌"=>"瓯",
"產"=>"产",
"産"=>"产",
+"甦"=>"苏",
"畝"=>"亩",
"畢"=>"毕",
"異"=>"异",
@@ -5576,6 +8157,7 @@ $zh2Hans = array(
"稅"=>"税",
"稈"=>"秆",
"稏"=>"䅉",
+"稜"=>"棱",
"稟"=>"禀",
"種"=>"种",
"稱"=>"称",
@@ -5629,11 +8211,11 @@ $zh2Hans = array(
"簾"=>"帘",
"籃"=>"篮",
"籌"=>"筹",
-"籖"=>"签",
"籙"=>"箓",
"籜"=>"箨",
"籟"=>"籁",
"籠"=>"笼",
+"籤"=>"签",
"籩"=>"笾",
"籪"=>"簖",
"籬"=>"篱",
@@ -5673,6 +8255,7 @@ $zh2Hans = array(
"紝"=>"纴",
"紡"=>"纺",
"紬"=>"䌷",
+"紮"=>"扎",
"細"=>"细",
"紱"=>"绂",
"紲"=>"绁",
@@ -5897,7 +8480,6 @@ $zh2Hans = array(
"芻"=>"刍",
"苧"=>"苎",
"苹"=>"苹",
-"范"=>"范",
"茲"=>"兹",
"荊"=>"荆",
"莊"=>"庄",
@@ -5911,7 +8493,6 @@ $zh2Hans = array(
"萵"=>"莴",
"葉"=>"叶",
"葒"=>"荭",
-"著"=>"着",
"葤"=>"荮",
"葦"=>"苇",
"葯"=>"药",
@@ -5927,6 +8508,7 @@ $zh2Hans = array(
"蓴"=>"莼",
"蓽"=>"荜",
"蔔"=>"卜",
+"蔘"=>"参",
"蔞"=>"蒌",
"蔣"=>"蒋",
"蔥"=>"葱",
@@ -5948,6 +8530,7 @@ $zh2Hans = array(
"薈"=>"荟",
"薊"=>"蓟",
"薌"=>"芗",
+"薑"=>"姜",
"薔"=>"蔷",
"薘"=>"荙",
"薟"=>"莶",
@@ -5956,7 +8539,6 @@ $zh2Hans = array(
"薳"=>"䓕",
"薴"=>"苧",
"薺"=>"荠",
-"藉"=>"借",
"藍"=>"蓝",
"藎"=>"荩",
"藝"=>"艺",
@@ -6084,6 +8666,7 @@ $zh2Hans = array(
"訓"=>"训",
"訕"=>"讪",
"訖"=>"讫",
+"託"=>"托",
"託"=>"讬",
"記"=>"记",
"訛"=>"讹",
@@ -6221,6 +8804,7 @@ $zh2Hans = array(
"譚"=>"谭",
"譜"=>"谱",
"譫"=>"谵",
+"譭"=>"毁",
"譯"=>"译",
"議"=>"议",
"譴"=>"谴",
@@ -6236,6 +8820,7 @@ $zh2Hans = array(
"讓"=>"让",
"讕"=>"谰",
"讖"=>"谶",
+"讚"=>"赞",
"讜"=>"谠",
"讞"=>"谳",
"豈"=>"岂",
@@ -6406,7 +8991,6 @@ $zh2Hans = array(
"轡"=>"辔",
"轢"=>"轹",
"轤"=>"轳",
-"辟"=>"辟",
"辦"=>"办",
"辭"=>"辞",
"辮"=>"辫",
@@ -6440,7 +9024,6 @@ $zh2Hans = array(
"邊"=>"边",
"邏"=>"逻",
"邐"=>"逦",
-"郁"=>"郁",
"郟"=>"郏",
"郵"=>"邮",
"鄆"=>"郓",
@@ -6467,7 +9050,6 @@ $zh2Hans = array(
"釁"=>"衅",
"釃"=>"酾",
"釅"=>"酽",
-"采"=>"采",
"釋"=>"释",
"釐"=>"厘",
"釒"=>"钅",
@@ -6636,7 +9218,6 @@ $zh2Hans = array(
"鍵"=>"键",
"鍶"=>"锶",
"鍺"=>"锗",
-"鍾"=>"钟",
"鎂"=>"镁",
"鎄"=>"锿",
"鎇"=>"镅",
@@ -6740,6 +9321,7 @@ $zh2Hans = array(
"閎"=>"闳",
"閏"=>"闰",
"閑"=>"闲",
+"閒"=>"闲",
"間"=>"间",
"閔"=>"闵",
"閘"=>"闸",
@@ -6778,11 +9360,13 @@ $zh2Hans = array(
"闞"=>"阚",
"闠"=>"阓",
"闡"=>"阐",
+"闢"=>"辟",
"闤"=>"阛",
"闥"=>"闼",
"阪"=>"坂",
"陘"=>"陉",
"陝"=>"陕",
+"陞"=>"升",
"陣"=>"阵",
"陰"=>"阴",
"陳"=>"陈",
@@ -6933,6 +9517,7 @@ $zh2Hans = array(
"餓"=>"饿",
"餕"=>"馂",
"餖"=>"饾",
+"餘"=>"余",
"餚"=>"肴",
"餛"=>"馄",
"餜"=>"馃",
@@ -6941,6 +9526,7 @@ $zh2Hans = array(
"館"=>"馆",
"餱"=>"糇",
"餳"=>"饧",
+"餵"=>"喂",
"餶"=>"馉",
"餷"=>"馇",
"餺"=>"馎",
@@ -7039,6 +9625,7 @@ $zh2Hans = array(
"鬢"=>"鬓",
"鬥"=>"斗",
"鬧"=>"闹",
+"鬨"=>"哄",
"鬩"=>"阋",
"鬮"=>"阄",
"鬱"=>"郁",
@@ -7191,6 +9778,7 @@ $zh2Hans = array(
"鵬"=>"鹏",
"鵮"=>"鹐",
"鵯"=>"鹎",
+"鵰"=>"雕",
"鵲"=>"鹊",
"鵷"=>"鹓",
"鵾"=>"鹍",
@@ -7250,10 +9838,13 @@ $zh2Hans = array(
"鹵"=>"卤",
"鹹"=>"咸",
"鹺"=>"鹾",
+"鹼"=>"硷",
"鹽"=>"盐",
"麗"=>"丽",
"麥"=>"麦",
"麩"=>"麸",
+"麪"=>"面",
+"麫"=>"面",
"麯"=>"曲",
"麵"=>"面",
"麼"=>"么",
@@ -7298,48 +9889,455 @@ $zh2Hans = array(
"龕"=>"龛",
"龜"=>"龟",
+"一畫" => "一画",
+"一著不慎" => "一着不慎",
+"三聯畫" => "三联画",
+"上畫" => "上画",
+"不著" => "不着",
+"不著痕跡" => "不着痕迹",
+"不著邊際" => "不着边际",
+"與著" => "与着",
+"與著作" => "与著作",
+"與著名" => "与著名",
+"與著者" => "与著者",
+"丑著" => "丑着",
+"臨著" => "临着",
+"為著" => "为着",
+"麗著" => "丽着",
+"舉著" => "举着",
+"樂著" => "乐着",
+"樂著作" => "乐著作",
+"乘著" => "乘着",
+"書畫" => "书画",
+"乾乾" => "乾乾",
+"乾元;" => "乾元;",
+"乾卦;" => "乾卦;",
+"乾縣" => "乾县",
+"乾嘉" => "乾嘉",
+"乾圖" => "乾图",
+"乾坤 " => "乾坤 ",
+"乾宅;" => "乾宅;",
+"乾斷" => "乾断",
+"乾旦" => "乾旦",
+"乾曜;" => "乾曜;",
+"乾清宮" => "乾清宫",
+"乾盛世" => "乾盛世",
+"乾紅" => "乾红",
+"乾綱" => "乾纲",
+"乾象;" => "乾象;",
+"乾造;" => "乾造;",
+"乾陵" => "乾陵",
+"乾隆" => "乾隆",
+"爭著" => "争着",
+"交叉著" => "交叉着",
+"亮著" => "亮着",
+"仗著" => "仗着",
+"代表著" => "代表着",
+"代表著作" => "代表著作",
+"代表著名" => "代表著名",
+"代表著者" => "代表著者",
+"仰著" => "仰着",
+"傷著" => "伤着",
+"伴著" => "伴着",
+"低著" => "低着",
+"住著" => "住着",
+"佛頭著糞" => "佛头着粪",
+"作畫" => "作画",
+"側著" => "侧着",
+"保障著" => "保障着",
+"保障著作" => "保障著作",
+"保障著名" => "保障著名",
+"保障著者" => "保障著者",
+"信著" => "信着",
+"候著" => "候着",
+"借著" => "借着",
+"偎著" => "偎着",
+"做著" => "做着",
+"停著" => "停着",
+"偷著" => "偷着",
+"先人著鞭" => "先人着鞭",
+"先我著鞭" => "先我着鞭",
+"光著" => "光着",
+"光著作" => "光著作",
+"入畫" => "入画",
+"關著" => "关着",
+"關著作" => "关著作",
+"關著名" => "关著名",
+"關著者" => "关著者",
+"冀著" => "冀着",
+"冒著" => "冒着",
+"寫生畫" => "写生画",
+"寫著" => "写着",
+"寫著作" => "写著作",
+"寫著名" => "写著名",
+"沖著" => "冲着",
+"涼著" => "凉着",
"幾畫" => "几画",
+"憑著" => "凭着",
+"制著" => "制着",
+"刻畫" => "刻画",
+"刻著" => "刻着",
+"剃著" => "剃着",
+"剪著" => "剪着",
+"辦著" => "办着",
+"動畫" => "动画",
+"動著" => "动着",
+"努力著" => "努力着",
+"努著" => "努着",
+"勾畫" => "勾画",
+"包著" => "包着",
+"單色畫" => "单色画",
"賣畫" => "卖画",
-"滷鹼" => "卤碱",
+"卡通畫" => "卡通画",
+"鹵鹼" => "卤碱",
+"印著" => "印着",
+"卷舌" => "卷舌",
+"壓著" => "压着",
"原畫" => "原画",
+"去著" => "去着",
+"受著" => "受着",
+"受著作" => "受著作",
+"受著名" => "受著名",
+"受著者" => "受著者",
+"變著" => "变着",
"口鹼" => "口碱",
"古畫" => "古画",
+"叫喊著說" => "叫喊着说",
+"叫著" => "叫着",
+"吃著" => "吃着",
"名畫" => "名画",
+"向著" => "向着",
+"嚇著" => "吓着",
+"含著" => "含着",
+"含著作" => "含著作",
+"含著名" => "含著名",
+"含著者" => "含著者",
+"聽著" => "听着",
+"聽著作" => "听著作",
+"聽著名" => "听著名",
+"聽著者" => "听著者",
+"吸著劑" => "吸着剂",
+"吸著物" => "吸着物",
+"吹著" => "吹着",
+"呆著" => "呆着",
+"嗆著" => "呛着",
+"味著" => "味着",
+"咧著" => "咧着",
+"咬著" => "咬着",
+"響著" => "响着",
+"哭喪著臉" => "哭丧着脸",
+"哭著" => "哭着",
+"哼著唱" => "哼着唱",
+"唱著" => "唱着",
+"啃著" => "啃着",
+"喝著" => "喝着",
+"噙著" => "噙着",
+"嚷著" => "嚷着",
+"囔著" => "囔着",
+"因著" => "因着",
+"困著" => "困着",
+"圍著" => "围着",
+"固著" => "固着",
+"國畫" => "国画",
+"圖畫" => "图画",
+"在著" => "在着",
+"在著作" => "在著作",
+"在著名" => "在著名",
+"在著者" => "在著者",
+"坐著" => "坐着",
+"墊著" => "垫着",
+"埋著" => "埋着",
+"壁畫" => "壁画",
+"備著" => "备着",
+"失著" => "失着",
+"夾著" => "夹着",
"奇畫" => "奇画",
"如畫" => "如画",
+"字畫" => "字画",
+"孤著" => "孤着",
+"學著" => "学着",
+"學著作" => "学著作",
+"守著" => "守着",
+"定著" => "定着",
+"定著作" => "定著作",
+"宣傳畫" => "宣传画",
+"對著" => "对着",
+"對著干" => "对着干",
+"對著作" => "对著作",
+"對著名" => "对著名",
+"對著者" => "对著者",
+"尋著" => "寻着",
+"就著" => "就着",
+"展著" => "展着",
+"帶著" => "带着",
+"幫著" => "帮着",
+"年畫" => "年画",
+"幽默畫" => "幽默画",
+"應著" => "应着",
+"康乾" => "康乾",
+"康著" => "康着",
+"開著" => "开着",
+"弄著" => "弄着",
+"引著" => "引着",
+"張法乾" => "张法乾",
"弱鹼" => "弱碱",
+"彈著點 " => "弹着点 ",
+"歸著 " => "归着 ",
+"當著" => "当着",
+"當著 " => "当着 ",
"彩畫" => "彩画",
+"待著" => "待着",
+"得著" => "得着",
+"得著作" => "得著作",
+"得著名" => "得著名",
+"得著者" => "得著者",
+"循著" => "循着",
+"心著" => "心着",
+"忍著" => "忍着",
+"志著" => "志着",
+"忙著" => "忙着",
+"念著" => "念着",
+"懷著" => "怀着",
+"怎么著" => "怎么着",
+"急著" => "急着",
+"性著" => "性着",
+"性著作" => "性著作",
+"性著名" => "性著名",
+"性著者" => "性著者",
+"性著述" => "性著述",
+"戀著" => "恋着",
+"悠著" => "悠着",
+"懸著" => "悬着",
+"惦著" => "惦着",
+"慣著" => "惯着",
+"想著" => "想着",
+"意味著" => "意味着",
+"愣著" => "愣着",
+"慢著" => "慢着",
+"憋著" => "憋着",
+"戰著" => "战着",
+"戴著" => "戴着",
"所畫" => "所画",
"扉畫" => "扉画",
+"扎著" => "扎着",
+"打著" => "打着",
+"托著" => "托着",
+"扛著" => "扛着",
+"執著" => "执着",
+"扭著" => "扭着",
+"找不著" => "找不着",
+"找著 " => "找着 ",
+"抓著" => "抓着",
+"抗著" => "抗着",
+"搶著" => "抢着",
+"披著" => "披着",
+"抬著" => "抬着",
+"抱著" => "抱着",
+"押著" => "押着",
+"拄著" => "拄着",
+"拉著" => "拉着",
+"拍著" => "拍着",
+"拎著" => "拎着",
+"拎著 " => "拎着 ",
+"拖著" => "拖着",
+"拙著" => "拙着",
+"擁著" => "拥着",
+"拼著" => "拼着",
+"拽著" => "拽着",
+"拿著" => "拿着",
+"持著" => "持着",
+"掛著" => "挂着",
+"指畫" => "指画",
+"指著" => "指着",
+"按著" => "按着",
+"挎著" => "挎着",
+"挑著" => "挑着",
+"擋著" => "挡着",
+"掙著" => "挣着",
+"擠著" => "挤着",
+"揮著" => "挥着",
+"挨著" => "挨着",
+"挺著" => "挺着",
+"捂著" => "捂着",
+"捆著" => "捆着",
+"捏著" => "捏着",
+"撈著" => "捞着",
+"撿著" => "捡着",
+"捧著" => "捧着",
+"據著" => "据着",
+"據著作" => "据著作",
+"據著名" => "据著名",
+"據著者" => "据著者",
+"掖著" => "掖着",
+"接著" => "接着",
+"推著" => "推着",
+"揉著" => "揉着",
+"描畫" => "描画",
+"提著" => "提着",
+"插畫" => "插画",
+"握著" => "握着",
+"摟著" => "搂着",
+"擺著" => "摆着",
+"搖著" => "摇着",
+"摸著" => "摸着",
+"撼著" => "撼着",
+"擎著" => "擎着",
+"擘畫 " => "擘画 ",
+"攀著" => "攀着",
+"攥著" => "攥着",
+"支著" => "支着",
+"放著" => "放着",
"教畫" => "教画",
+"敞著" => "敞着",
+"數著" => "数着",
+"斗著" => "斗着",
+"斥著" => "斥着",
+"新著龍虎門" => "新著龙虎门",
+"於乎" => "於乎",
+"於夫羅" => "於夫罗",
+"於姓" => "於姓",
+"於戲" => "於戏",
+"於梨華" => "於梨华",
+"於氏" => "於氏",
+"於潛縣" => "於潜县",
+"於菟" => "於菟",
+"旋乾轉坤" => "旋乾转坤",
+"無著" => "无着",
+"昂著" => "昂着",
+"映著" => "映着",
+"春畫" => "春画",
+"昧著" => "昧着",
+"晃著" => "晃着",
+"暗著" => "暗着",
+"有著" => "有着",
+"有著作" => "有著作",
+"有著名" => "有著名",
+"有著者" => "有著者",
+"望著" => "望着",
+"朝著" => "朝着",
+"本著" => "本着",
+"本著作" => "本著作",
+"本著名" => "本著名",
+"本著者" => "本著者",
+"機械畫" => "机械画",
+"雜著" => "杂着",
+"李乾德;" => "李乾德;",
+"來著" => "来着",
+"板著臉" => "板着脸",
+"枕著" => "枕着",
+"柳詒徵" => "柳诒徵",
+"標志著" => "标志着",
+"夢著" => "梦着",
+"梳著" => "梳着",
+"棋高一著" => "棋高一着",
+"樊於期" => "樊於期",
+"歇著" => "歇着",
+"歪打正著" => "歪打正着",
+"比畫 " => "比画 ",
+"比著" => "比着",
"水鹼" => "水碱",
+"水粉畫" => "水粉画",
+"求著" => "求着",
+"沉著" => "沉着",
+"油畫" => "油画",
+"沿著" => "沿着",
"洋鹼" => "洋碱",
+"活著" => "活着",
+"流著" => "流着",
+"浮著" => "浮着",
+"海景畫" => "海景画",
+"塗畫" => "涂画",
+"塗著" => "涂着",
+"涎著臉" => "涎着脸",
+"潤著" => "润着",
+"涵著" => "涵着",
+"涵著作" => "涵著作",
+"涵著名" => "涵著名",
+"涵著者" => "涵著者",
+"渴著" => "渴着",
+"溢著" => "溢着",
+"演著" => "演着",
+"演著作" => "演著作",
+"演著名" => "演著名",
+"演著者" => "演著者",
+"漫畫" => "漫画",
+"漫著" => "漫着",
+"潛伏著 " => "潜伏着 ",
"炭畫" => "炭画",
+"點畫" => "点画",
+"點著" => "点着",
+"煙鹼" => "烟碱",
+"烤著" => "烤着",
+"燒著" => "烧着",
+"燒鹼" => "烧碱",
+"照著" => "照着",
+"照著作" => "照著作",
+"照著名" => "照著名",
+"照著者" => "照著者",
+"愛著" => "爱着",
+"愛鬧著玩" => "爱闹着玩",
+"版畫" => "版画",
+"牽著" => "牵着",
+"犯不著" => "犯不着",
+"獨著" => "独着",
+"猜著" => "猜着",
+"猜著 " => "猜着 ",
+"玩著" => "玩着",
+"甜著" => "甜着",
+"用不著" => "用不着",
+"用著" => "用着",
+"用著作" => "用著作",
+"畫 " => "画 ",
"畫一" => "画一",
"畫上" => "画上",
"畫下" => "画下",
"畫中" => "画中",
+"畫了" => "画了",
"畫供" => "画供",
+"畫像" => "画像",
"畫兒" => "画儿",
"畫具" => "画具",
+"畫冊" => "画册",
"畫出" => "画出",
+"畫刊" => "画刊",
+"畫匠" => "画匠",
+"畫卷" => "画卷",
"畫史" => "画史",
"畫品" => "画品",
"畫商" => "画商",
+"畫圖" => "画图",
"畫圈" => "画圈",
+"畫壇" => "画坛",
"畫境" => "画境",
+"畫外" => "画外",
+"畫室" => "画室",
+"畫家" => "画家",
+"畫屏 " => "画屏 ",
+"畫展" => "画展",
"畫工" => "画工",
+"畫布" => "画布",
+"畫師 " => "画师 ",
"畫帖" => "画帖",
"畫幅" => "画幅",
+"畫廊" => "画廊",
"畫意" => "画意",
"畫成" => "画成",
+"畫報" => "画报",
+"畫押" => "画押",
"畫景" => "画景",
"畫本" => "画本",
+"畫板 " => "画板 ",
"畫架" => "画架",
"畫框" => "画框",
"畫法" => "画法",
+"畫片" => "画片",
"畫王" => "画王",
+"畫畫" => "画画",
"畫界" => "画界",
+"畫皮" => "画皮",
+"畫眉" => "画眉",
+"畫稿" => "画稿",
+"畫筆" => "画笔",
"畫符" => "画符",
"畫紙" => "画纸",
"畫線" => "画线",
@@ -7352,106 +10350,285 @@ $zh2Hans = array(
"畫質" => "画质",
"畫貼" => "画贴",
"畫軸" => "画轴",
+"畫院" => "画院",
+"畫集 " => "画集 ",
+"畫面" => "画面",
"畫頁" => "画页",
+"留著" => "留着",
+"疑著" => "疑着",
+"皺著" => "皱着",
"鹽鹼" => "盐碱",
+"盛著" => "盛着",
+"盯著" => "盯着",
+"直著" => "直着",
+"盼著" => "盼着",
+"盾著" => "盾着",
+"看著" => "看着",
+"眯著" => "眯着",
+"著 " => "着 ",
+"著絲" => "着丝",
+"著人先鞭" => "着人先鞭",
+"著什么急" => "着什么急",
+"著他" => "着他",
+"著你" => "着你",
+"著兒" => "着儿",
+"著涼" => "着凉",
+"著力" => "着力",
+"著呢 " => "着呢 ",
+"著地" => "着地",
+"著墨" => "着墨",
+"著她" => "着她",
+"著妳" => "着妳",
+"著它" => "着它",
+"著實" => "着实",
+"著床" => "着床",
+"著錄" => "着录",
+"著忙" => "着忙",
+"著急" => "着急",
+"著惱" => "着恼",
+"著想" => "着想",
+"著意" => "着意",
+"著慌" => "着慌",
+"著我" => "着我",
+"著手" => "着手",
+"著數" => "着数",
+"著棋 " => "着棋 ",
+"著法" => "着法",
+"著火" => "着火",
+"著眼" => "着眼",
+"著祂" => "着祂",
+"著筆" => "着笔",
+"著緊" => "着紧",
+"著腳" => "着脚",
+"著艦" => "着舰",
+"著色" => "着色",
+"著花" => "着花",
+"著落" => "着落",
+"著衣" => "着衣",
+"著裝" => "着装",
+"著迷" => "着迷",
+"著重" => "着重",
+"著陸" => "着陆",
+"著鞭" => "着鞭",
+"著魔" => "着魔",
+"睜著" => "睁着",
+"睡不著" => "睡不着",
+"睡著" => "睡着",
+"瞄著" => "瞄着",
+"瞅著" => "瞅着",
+"瞞著" => "瞒着",
+"瞧著" => "瞧着",
+"瞪著" => "瞪着",
+"知疼著熱" => "知疼着热",
+"硝鹼" => "硝碱",
+"硬著" => "硬着",
"鹼 " => "碱 ",
+"鹼化" => "碱化",
+"鹼場" => "碱场",
"鹼基" => "碱基",
"鹼度" => "碱度",
+"鹼性" => "碱性",
"鹼水" => "碱水",
"鹼熔" => "碱熔",
+"鹼類" => "碱类",
"磁畫" => "磁画",
+"福著" => "福着",
+"積著" => "积着",
+"空著" => "空着",
+"穿著" => "穿着",
+"立著" => "立着",
+"豎著" => "竖着",
+"站著" => "站着",
+"端著" => "端着",
+"笑著" => "笑着",
+"筆畫" => "笔画",
+"等著" => "等着",
"策畫" => "策画",
+"管著" => "管着",
+"粘著" => "粘着",
+"系著" => "系着",
+"緊著" => "紧着",
+"純鹼" => "纯碱",
"組畫" => "组画",
+"細密畫" => "细密画",
+"綁著" => "绑着",
+"繞著" => "绕着",
+"繪畫" => "绘画",
"絹畫" => "绢画",
+"綳著勁" => "绷着劲",
+"綳著臉 " => "绷着脸 ",
+"纏著" => "缠着",
+"罩著" => "罩着",
+"美著" => "美着",
+"美著作" => "美著作",
+"美著名" => "美著名",
+"美著者" => "美著者",
+"耀著" => "耀着",
+"老著臉皮" => "老着脸皮",
+"考著" => "考着",
"耐鹼" => "耐碱",
+"聊著" => "聊着",
"肉鹼" => "肉碱",
+"肖像畫" => "肖像画",
+"背著" => "背着",
"膠畫" => "胶画",
+"膠著" => "胶着",
+"腆著" => "腆着",
+"舞著" => "舞着",
+"藝著" => "艺着",
+"苦著" => "苦着",
"茶鹼" => "茶碱",
+"獲著" => "获着",
+"蕭乾" => "萧乾",
+"落著" => "落着",
+"蒙著" => "蒙着",
+"藏著" => "藏着",
+"蘸著" => "蘸着",
+"行著" => "行着",
+"衣著" => "衣着",
+"裝著" => "装着",
+"裸體畫" => "裸体画",
+"裹著" => "裹着",
+"西洋畫" => "西洋画",
"西畫" => "西画",
+"見著" => "见着",
+"覺著" => "觉着",
+"記著" => "记着",
+"講著" => "讲着",
+"試著" => "试着",
+"該著" => "该着",
+"語著" => "语着",
+"說著" => "说着",
+"豫著" => "豫着",
+"貞著" => "贞着",
"貼畫" => "贴画",
+"貼著" => "贴着",
+"貿著之仇" => "贸着之仇",
+"走著" => "走着",
+"趕著" => "赶着",
+"起著" => "起着",
+"趁著" => "趁着",
+"趴著" => "趴着",
+"躍著" => "跃着",
+"跐著腳 " => "跐着脚 ",
+"跑著" => "跑着",
+"跟著" => "跟着",
+"跪著" => "跪着",
+"跳著" => "跳着",
+"踏著" => "踏着",
+"踩著" => "踩着",
+"蹲著 " => "蹲着 ",
+"身著" => "身着",
+"躺著" => "躺着",
+"轉著" => "转着",
+"載著" => "载着",
+"達著" => "达着",
+"過著" => "过着",
+"邁著" => "迈着",
+"迎著" => "迎着",
"返鹼" => "返碱",
+"這么著" => "这么着",
+"遠著" => "远着",
+"連環畫" => "连环画",
+"連著" => "连着",
+"連著作" => "连著作",
+"連著名" => "连著名",
+"連著者" => "连著者",
+"追著" => "追着",
+"逆著" => "逆着",
+"透著" => "透着",
+"透視畫" => "透视画",
+"逼著" => "逼着",
+"遇著" => "遇着",
+"那么著" => "那么着",
+"郭子乾" => "郭子乾",
+"配著" => "配着",
+"酸鹼" => "酸碱",
+"釀著" => "酿着",
+"醒著" => "醒着",
+"鋪著" => "铺着",
+"鍾 " => "锺 ",
"鍾鍛" => "锺锻",
"鍛鍾" => "锻锺",
+"長著" => "长着",
+"閉著" => "闭着",
+"問著" => "问着",
+"閑著" => "闲着",
+"鬧著玩兒" => "闹着玩儿",
+"附著" => "附着",
+"陋著" => "陋着",
+"陪著" => "陪着",
+"隨著" => "随着",
+"隔著" => "隔着",
+"雅著" => "雅着",
"雕畫" => "雕画",
-"鯰 " => "鲶 ",
-"三聯畫" => "三联画",
-"中國畫" => "中国画",
-"書畫 " => "书画 ",
-"書畫社" => "书画社",
-"五筆畫" => "五笔画",
-"作畫 " => "作画 ",
-"入畫 " => "入画 ",
-"寫生畫" => "写生画",
-"刻畫 " => "刻画 ",
-"動畫 " => "动画 ",
-"勾畫 " => "勾画 ",
-"單色畫" => "单色画",
-"卡通畫" => "卡通画",
-"國畫 " => "国画 ",
-"圖畫 " => "图画 ",
-"壁畫 " => "壁画 ",
-"字畫 " => "字画 ",
-"宣傳畫" => "宣传画",
-"工筆畫" => "工笔画",
-"年畫 " => "年画 ",
-"幽默畫" => "幽默画",
-"指畫 " => "指画 ",
-"描畫 " => "描画 ",
-"插畫 " => "插画 ",
-"擘畫 " => "擘画 ",
-"春畫 " => "春画 ",
-"木刻畫" => "木刻画",
-"機械畫" => "机械画",
-"比畫 " => "比画 ",
-"毛筆畫" => "毛笔画",
-"水粉畫" => "水粉画",
-"油畫 " => "油画 ",
-"海景畫" => "海景画",
-"漫畫 " => "漫画 ",
-"點畫 " => "点画 ",
-"版畫 " => "版画 ",
-"畫 " => "画 ",
-"畫像 " => "画像 ",
-"畫冊 " => "画册 ",
-"畫刊 " => "画刊 ",
-"畫匠 " => "画匠 ",
-"畫捲 " => "画卷 ",
-"畫圖 " => "画图 ",
-"畫壇 " => "画坛 ",
-"畫室 " => "画室 ",
-"畫家 " => "画家 ",
-"畫屏 " => "画屏 ",
-"畫展 " => "画展 ",
-"畫布 " => "画布 ",
-"畫師 " => "画师 ",
-"畫廊 " => "画廊 ",
-"畫報 " => "画报 ",
-"畫押 " => "画押 ",
-"畫板 " => "画板 ",
-"畫片 " => "画片 ",
-"畫畫 " => "画画 ",
-"畫皮 " => "画皮 ",
-"畫眉鳥" => "画眉鸟",
-"畫稿 " => "画稿 ",
-"畫筆 " => "画笔 ",
-"畫院 " => "画院 ",
-"畫集 " => "画集 ",
-"畫面 " => "画面 ",
-"筆畫 " => "笔画 ",
-"細密畫" => "细密画",
-"繪畫 " => "绘画 ",
-"自畫像" => "自画像",
-"蠟筆畫" => "蜡笔画",
-"裸體畫" => "裸体画",
-"西洋畫" => "西洋画",
-"透視畫" => "透视画",
-"銅版畫" => "铜版画",
-"鍾 " => "锺 ",
"靜物畫" => "静物画",
+"靠著" => "靠着",
+"面對著 " => "面对着 ",
+"頂著" => "顶着",
+"順著" => "顺着",
+"顧著" => "顾着",
+"領著" => "领着",
+"風景畫" => "风景画",
+"飄著" => "飘着",
"餘 " => "馀 ",
+"餘年" => "馀年",
+"駕著" => "驾着",
+"罵著" => "骂着",
+"騎著" => "骑着",
+"騙著" => "骗着",
+"高著" => "高着",
+"髭著" => "髭着",
+"魏徵" => "魏徵",
+"鯰 " => "鲶 ",
+"鯰魚" => "鲶鱼",
+"黏著" => "黏着",
+"龕著" => "龛着",
+"乾县" => "乾县",
+"萧乾" => "萧乾",
+"乾断" => "乾断",
+"乾图" => "乾图",
+"乾纲" => "乾纲",
+"乾红" => "乾红",
+"乾清宫" => "乾清宫",
+"柳诒徵" => "柳诒徵",
+"於夫罗" => "於夫罗",
+"於梨华" => "於梨华",
+"于潜县" => "於潜县",
+"憑藉" => "凭借",
+"藉端" => "借端",
+"藉故" => "借故",
+"藉口" => "借口",
+"藉助" => "借助",
+"藉手" => "借手",
+"藉詞" => "借词",
+"藉機" => "借机",
+"藉此" => "借此",
+"藉由" => "借由",
);
$zh2TW = array(
+"”" => "」",
+"“" => "「",
+"‘" => "『",
+"’" => "』",
+"着" => "著",
+"元凶" => "元凶",
+"凶器" => "凶器",
+"凶徒" => "凶徒",
+"凶手" => "凶手",
+"凶案" => "凶案",
+"凶残" => "凶殘",
+"凶杀" => "凶殺",
+"疑凶" => "疑凶",
+"真凶" => "真凶",
+"缉凶" => "緝凶",
+"行凶" => "行凶",
+"行凶后" => "行凶後",
+"买凶" => "買凶",
+"追凶" => "追凶",
+"复苏" => "復甦",
+"復蘇" => "復甦",
"缺省" => "預設",
"串行" => "串列",
"以太网" => "乙太網",
@@ -7469,10 +10646,12 @@ $zh2TW = array(
"脱机" => "離線",
"声卡" => "音效卡",
"老字号" => "老字號",
+"连字号" => "連字號",
"字号" => "字型大小",
"字库" => "字型檔",
"字段" => "欄位",
"字符" => "字元",
+"字符集" => "字符集",
"存盘" => "存檔",
"寻址" => "定址",
"尾注" => "章節附註",
@@ -7489,6 +10668,9 @@ $zh2TW = array(
"磁盘" => "磁碟",
"磁道" => "磁軌",
"程控" => "程式控制",
+"远程控制" => "遠程控制",
+"遠程控制" => "遠程控制",
+"行程控制" => "行程控制",
"端口" => "埠",
"算子" => "運算元",
"算法" => "演算法",
@@ -7520,11 +10702,13 @@ $zh2TW = array(
"奶酪" => "乳酪",
"巨商" => "鉅賈",
"手电" => "手電筒",
+"手电筒" => "手電筒",
"万历" => "萬曆",
"永历" => "永曆",
"词汇" => "辭彙",
"习用" => "慣用",
"元音" => "母音",
+"宋元" => "宋元",
"任意球" => "自由球",
"头球" => "頭槌",
"入球" => "進球",
@@ -7549,6 +10733,7 @@ $zh2TW = array(
"網絡" => "網路",
"人工智能" => "人工智慧",
"航天飞机" => "太空梭",
+"航天大学" => "航天大學",
"穿梭機" => "太空梭",
"因特网" => "網際網路",
"互聯網" => "網際網路",
@@ -7713,6 +10898,8 @@ $zh2TW = array(
"快速面" => "速食麵",
"即食麵" => "速食麵",
"薯仔" => "土豆",
+"土豆网" => "土豆網",
+"土豆網" => "土豆網",
"蹦极跳" => "笨豬跳",
"绑紧跳" => "笨豬跳",
"冷菜" => "冷盤",
@@ -7723,6 +10910,8 @@ $zh2TW = array(
"雪糕" => "冰淇淋",
"卫生" => "衛生",
"衞生" => "衛生",
+"平治之亂" => "平治之亂",
+"平治之乱" => "平治之亂",
"平治" => "賓士",
"奔驰" => "賓士",
"積架" => "捷豹",
@@ -7741,10 +10930,17 @@ $zh2TW = array(
"凡高" => "梵谷",
"狄安娜" => "黛安娜",
"戴安娜" => "黛安娜",
-"赫拉" => "希拉",
);
$zh2HK = array(
+"”" => "」",
+"“" => "「",
+"‘" => "『",
+"’" => "』",
+"凶殺" => "兇殺",
+"凶殘" => "兇殘",
+"緝凶" => "緝兇",
+"買凶" => "買兇",
"打印机" => "打印機",
"印表機" => "打印機",
"字节" => "位元組",
@@ -7915,6 +11111,8 @@ $zh2HK = array(
"速食麵" => "即食麵",
"泡麵" => "即食麵",
"土豆" => "馬鈴薯",
+"土豆网" => "土豆網",
+"土豆網" => "土豆網",
"华乐" => "中樂",
"民乐" => "中樂",
"計程車" => "的士",
@@ -7953,6 +11151,10 @@ $zh2HK = array(
);
$zh2CN = array(
+"」" => "”",
+"「" => "“",
+"『" => "‘",
+"』" => "’",
"記憶體" => "内存",
"預設" => "默认",
"串列" => "串行",
@@ -8018,7 +11220,6 @@ $zh2CN = array(
"資料庫" => "数据库",
"乳酪" => "奶酪",
"鉅賈" => "巨商",
-"手電筒" => "手电",
"萬曆" => "万历",
"永曆" => "永历",
"辭彙" => "词汇",
@@ -8217,12 +11418,11 @@ $zh2CN = array(
"冷盤  " => "凉菜",
"冷菜" => "凉菜",
"散钱" => "零钱",
-"谐星" => "笑星    ",
+"谐星" => "笑星",
"夜学" => "夜校",
"华乐" => "民乐",
"中樂" => "民乐",
"屋价" => "房价",
-"的士" => "出租车",
"計程車" => "出租车",
"公車" => "公共汽车",
"單車" => "自行车",
@@ -8238,6 +11438,8 @@ $zh2CN = array(
"衛生" => "卫生",
"賓士" => "奔驰",
"平治" => "奔驰",
+"平治之亂" => "平治之乱",
+"平治之乱" => "平治之乱",
"積架" => "捷豹",
"福斯" => "大众",
"福士" => "大众",
@@ -8260,10 +11462,13 @@ $zh2CN = array(
"舒麥加" => "迈克尔·舒马赫",
"希特拉" => "希特勒",
"黛安娜" => "戴安娜",
-"希拉" => "赫拉",
);
$zh2SG = array(
+"」" => "”",
+"「" => "“",
+"『" => "‘",
+"』" => "’",
"方便面" => "快速面",
"速食麵" => "快速面",
"即食麵" => "快速面",
@@ -8279,4 +11484,5 @@ $zh2SG = array(
"住房" => "住屋",
"房价" => "屋价",
"泡麵" => "快速面",
+
); \ No newline at end of file
diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php
index 3a7b5099..732adae1 100644
--- a/includes/api/ApiBase.php
+++ b/includes/api/ApiBase.php
@@ -26,15 +26,15 @@
/**
* This abstract class implements many basic API functions, and is the base of all API classes.
* The class functions are divided into several areas of functionality:
- *
+ *
* Module parameters: Derived classes can define getAllowedParams() to specify which parameters to expect,
* how to parse and validate them.
- *
+ *
* Profiling: various methods to allow keeping tabs on various tasks and their time costs
- *
+ *
* Self-documentation: code to allow api to document its own state.
- *
- * @addtogroup API
+ *
+ * @ingroup API
*/
abstract class ApiBase {
@@ -68,18 +68,18 @@ abstract class ApiBase {
*****************************************************************************/
/**
- * Evaluates the parameters, performs the requested query, and sets up the
- * result. Concrete implementations of ApiBase must override this method to
+ * Evaluates the parameters, performs the requested query, and sets up the
+ * result. Concrete implementations of ApiBase must override this method to
* provide whatever functionality their module offers. Implementations must
* not produce any output on their own and are not expected to handle any
- * errors.
+ * errors.
*
* The execute method will be invoked directly by ApiMain immediately before
* the result of the module is output. Aside from the constructor, implementations
* should assume that no other methods will be called externally on the module
* before the result is processed.
*
- * The result data should be stored in the result object referred to by
+ * The result data should be stored in the result object referred to by
* "getResult()". Refer to ApiResult.php for details on populating a result
* object.
*/
@@ -93,21 +93,21 @@ abstract class ApiBase {
public abstract function getVersion();
/**
- * Get the name of the module being executed by this instance
+ * Get the name of the module being executed by this instance
*/
public function getModuleName() {
return $this->mModuleName;
}
/**
- * Get parameter prefix (usually two letters or an empty string).
+ * Get parameter prefix (usually two letters or an empty string).
*/
public function getModulePrefix() {
return $this->mModulePrefix;
- }
+ }
/**
- * Get the name of the module as shown in the profiler log
+ * Get the name of the module as shown in the profiler log
*/
public function getModuleProfileName($db = false) {
if ($db)
@@ -124,7 +124,7 @@ abstract class ApiBase {
}
/**
- * Returns true if this module is the main module ($this === $this->mMainModule),
+ * Returns true if this module is the main module ($this === $this->mMainModule),
* false otherwise.
*/
public function isMain() {
@@ -151,10 +151,17 @@ abstract class ApiBase {
}
/**
- * Set warning section for this module. Users should monitor this section to
+ * Set warning section for this module. Users should monitor this section to
* notice any changes in API.
*/
public function setWarning($warning) {
+ # If there is a warning already, append it to the existing one
+ $data =& $this->getResult()->getData();
+ if(isset($data['warnings'][$this->getModuleName()]))
+ {
+ $warning = "{$data['warnings'][$this->getModuleName()]['*']}\n$warning";
+ unset($data['warnings'][$this->getModuleName()]);
+ }
$msg = array();
ApiResult :: setContent($msg, $warning);
$this->getResult()->addValue('warnings', $this->getModuleName(), $msg);
@@ -163,7 +170,7 @@ abstract class ApiBase {
/**
* If the module may only be used with a certain format module,
* it should override this method to return an instance of that formatter.
- * A value of null means the default format will be used.
+ * A value of null means the default format will be used.
*/
public function getCustomPrinter() {
return null;
@@ -186,6 +193,9 @@ abstract class ApiBase {
);
$msg = $lnPrfx . implode($lnPrfx, $msg) . "\n";
+ if ($this->mustBePosted())
+ $msg .= "\nThis module only accepts POST requests.\n";
+
// Parameters
$paramsMsg = $this->makeHelpMsgParameters();
if ($paramsMsg !== false) {
@@ -207,7 +217,7 @@ abstract class ApiBase {
$versions = $this->getVersion();
$pattern = '(\$.*) ([0-9a-z_]+\.php) (.*\$)';
$replacement = '\\0' . "\n " . 'http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/includes/api/\\2';
-
+
if (is_array($versions)) {
foreach ($versions as &$v)
$v = eregi_replace($pattern, $replacement, $v);
@@ -223,7 +233,7 @@ abstract class ApiBase {
return $msg;
}
- /**
+ /**
* Generates the parameter descriptions for this module, to be displayed in the
* module's help.
*/
@@ -239,7 +249,7 @@ abstract class ApiBase {
if (is_array($desc))
$desc = implode($paramPrefix, $desc);
- $type = $paramSettings[self :: PARAM_TYPE];
+ $type = isset($paramSettings[self :: PARAM_TYPE])? $paramSettings[self :: PARAM_TYPE] : null;
if (isset ($type)) {
if (isset ($paramSettings[self :: PARAM_ISMULTI]))
$prompt = 'Values (separate with \'|\'): ';
@@ -274,7 +284,7 @@ abstract class ApiBase {
$intRangeStr = "The value must be no more than {$paramSettings[self :: PARAM_MAX]}";
else
$intRangeStr = "The value must be between {$paramSettings[self :: PARAM_MIN]} and {$paramSettings[self :: PARAM_MAX]}";
-
+
$desc .= $paramPrefix . $intRangeStr;
}
break;
@@ -324,7 +334,7 @@ abstract class ApiBase {
/**
* This method mangles parameter name based on the prefix supplied to the constructor.
- * Override this method to change parameter name during runtime
+ * Override this method to change parameter name during runtime
*/
public function encodeParamName($paramName) {
return $this->mModulePrefix . $paramName;
@@ -348,12 +358,12 @@ abstract class ApiBase {
}
/**
- * Get a value for the given parameter
+ * Get a value for the given parameter
*/
- protected function getParameter($paramName) {
+ protected function getParameter($paramName, $parseMaxLimit = true) {
$params = $this->getAllowedParams();
$paramSettings = $params[$paramName];
- return $this->getParameterFromSettings($paramName, $paramSettings);
+ return $this->getParameterFromSettings($paramName, $paramSettings, $parseMaxLimit);
}
/**
@@ -435,7 +445,7 @@ abstract class ApiBase {
$value = is_array($value) ? array_map('intval', $value) : intval($value);
$min = isset ($paramSettings[self :: PARAM_MIN]) ? $paramSettings[self :: PARAM_MIN] : null;
$max = isset ($paramSettings[self :: PARAM_MAX]) ? $paramSettings[self :: PARAM_MAX] : null;
-
+
if (!is_null($min) || !is_null($max)) {
$values = is_array($value) ? $value : array($value);
foreach ($values as $v) {
@@ -495,24 +505,41 @@ abstract class ApiBase {
/**
* Return an array of values that were given in a 'a|b|c' notation,
* after it optionally validates them against the list allowed values.
- *
+ *
* @param valueName - The name of the parameter (for error reporting)
* @param value - The value being parsed
* @param allowMultiple - Can $value contain more than one value separated by '|'?
* @param allowedValues - An array of values to check against. If null, all values are accepted.
- * @return (allowMultiple ? an_array_of_values : a_single_value)
+ * @return (allowMultiple ? an_array_of_values : a_single_value)
*/
protected function parseMultiValue($valueName, $value, $allowMultiple, $allowedValues) {
- $valuesList = explode('|', $value);
+ 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
+ }
if (!$allowMultiple && count($valuesList) != 1) {
$possibleValues = is_array($allowedValues) ? "of '" . implode("', '", $allowedValues) . "'" : '';
$this->dieUsage("Only one $possibleValues is allowed for parameter '$valueName'", "multival_$valueName");
}
if (is_array($allowedValues)) {
- $unknownValues = array_diff($valuesList, $allowedValues);
- if ($unknownValues) {
- $this->dieUsage('Unrecognised value' . (count($unknownValues) > 1 ? "s" : "") . " for parameter '$valueName'", "unknown_$valueName");
+ # Check for unknown values
+ $unknown = array_diff($valuesList, $allowedValues);
+ if(!empty($unknown))
+ {
+ if($allowMultiple)
+ {
+ $s = count($unknown) > 1 ? "s" : "";
+ $vals = implode(", ", $unknown);
+ $this->setWarning("Unrecognized value$s for parameter '$valueName': $vals");
+ }
+ else
+ $this->dieUsage("Unrecognized value for parameter '$valueName': {$valuesList[0]}", "unknown_$valueName");
}
+ # Now throw them out
+ $valuesList = array_intersect($valuesList, $allowedValues);
}
return $allowMultiple ? $valuesList : $valuesList[0];
@@ -544,12 +571,12 @@ abstract class ApiBase {
}
/**
- * Call main module's error handler
+ * Call main module's error handler
*/
public function dieUsage($description, $errorCode, $httpRespCode = 0) {
throw new UsageException($description, $this->encodeParamName($errorCode), $httpRespCode);
}
-
+
/**
* Array that maps message keys to error messages. $1 and friends are replaced.
*/
@@ -557,7 +584,7 @@ abstract class ApiBase {
// This one MUST be present, or dieUsageMsg() will recurse infinitely
'unknownerror' => array('code' => 'unknownerror', 'info' => "Unknown error: ``\$1''"),
'unknownerror-nocode' => array('code' => 'unknownerror', 'info' => 'Unknown error'),
-
+
// Messages from Title::getUserPermissionsErrors()
'ns-specialprotected' => array('code' => 'unsupportednamespace', 'info' => "Pages in the Special namespace can't be edited"),
'protectedinterface' => array('code' => 'protectednamespace-interface', 'info' => "You're not allowed to edit interface messages"),
@@ -578,11 +605,11 @@ abstract class ApiBase {
'confirmedittext' => array('code' => 'confirmemail', 'info' => "You must confirm your e-mail address before you can edit"),
'blockedtext' => array('code' => 'blocked', 'info' => "You have been blocked from editing"),
'autoblockedtext' => array('code' => 'autoblocked', 'info' => "Your IP address has been blocked automatically, because it was used by a blocked user"),
-
+
// Miscellaneous interface messages
'actionthrottledtext' => array('code' => 'ratelimited', 'info' => "You've exceeded your rate limit. Please wait some time and try again"),
'alreadyrolled' => array('code' => 'alreadyrolled', 'info' => "The page you tried to rollback was already rolled back"),
- 'cantrollback' => array('code' => 'onlyauthor', 'info' => "The page you tried to rollback only has one author"),
+ 'cantrollback' => array('code' => 'onlyauthor', 'info' => "The page you tried to rollback only has one author"),
'readonlytext' => array('code' => 'readonly', 'info' => "The wiki is currently in read-only mode"),
'sessionfailure' => array('code' => 'badtoken', 'info' => "Invalid token"),
'cannotdelete' => array('code' => 'cantdelete', 'info' => "Couldn't delete ``\$1''. Maybe it was deleted already by someone else"),
@@ -593,6 +620,8 @@ abstract class ApiBase {
'protectedpage' => array('code' => 'protectedpage', 'info' => "You don't have permission to perform this move"),
'hookaborted' => array('code' => 'hookaborted', 'info' => "The modification you tried to make was aborted by an extension hook"),
'cantmove-titleprotected' => array('code' => 'protectedtitle', 'info' => "The destination article has been protected from creation"),
+ 'imagenocrossnamespace' => array('code' => 'nonfilenamespace', 'info' => "Can't move a file to a non-file namespace"),
+ 'imagetypemismatch' => array('code' => 'filetypemismatch', 'info' => "The new file extension doesn't match its type"),
// 'badarticleerror' => shouldn't happen
// 'badtitletext' => shouldn't happen
'ip_range_invalid' => array('code' => 'invalidrange', 'info' => "Invalid IP range"),
@@ -603,7 +632,7 @@ 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"),
-
+
// API-specific messages
'missingparam' => array('code' => 'no$1', 'info' => "The \$1 parameter must be set"),
'invalidtitle' => array('code' => 'invalidtitle', 'info' => "Bad title ``\$1''"),
@@ -616,12 +645,28 @@ abstract class ApiBase {
'canthide' => array('code' => 'canthide', 'info' => "You don't have permission to hide user names from the block log"),
'cantblock-email' => array('code' => 'cantblock-email', 'info' => "You don't have permission to block users from sending e-mail through the wiki"),
'unblock-notarget' => array('code' => 'notarget', 'info' => "Either the id or the user parameter must be set"),
- 'unblock-idanduser' => array('code' => 'idanduser', 'info' => "The id and user parameters can\'t be used together"),
+ 'unblock-idanduser' => array('code' => 'idanduser', 'info' => "The id and user parameters can't be used together"),
'cantunblock' => array('code' => 'permissiondenied', 'info' => "You don't have permission to unblock users"),
'cannotundelete' => array('code' => 'cantundelete', 'info' => "Couldn't undelete: the requested revisions may not exist, or may have been undeleted already"),
'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"),
+
+ // ApiEditPage messages
+ 'noimageredirect-anon' => array('code' => 'noimageredirect-anon', 'info' => "Anonymous users can't create image redirects"),
+ 'noimageredirect-logged' => array('code' => 'noimageredirect', 'info' => "You don't have permission to create image redirects"),
+ 'spamdetected' => array('code' => 'spamdetected', 'info' => "Your edit was refused because it contained a spam fragment: ``\$1''"),
+ 'filtered' => array('code' => 'filtered', 'info' => "The filter callback function refused your edit"),
+ 'contenttoobig' => array('code' => 'contenttoobig', 'info' => "The content you supplied exceeds the article size limit of \$1 bytes"),
+ 'noedit-anon' => array('code' => 'noedit-anon', 'info' => "Anonymous users can't edit pages"),
+ 'noedit' => array('code' => 'noedit', 'info' => "You don't have permission to edit pages"),
+ 'wasdeleted' => array('code' => 'pagedeleted', 'info' => "The page has been deleted since you fetched its timestamp"),
+ 'blankpage' => array('code' => 'emptypage', 'info' => "Creating new, empty pages is not allowed"),
+ '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"),
);
-
+
/**
* Output the error message related to a certain array
* @param array $error Element of a getUserPermissionsErrors()
@@ -654,7 +699,7 @@ abstract class ApiBase {
public function isEditMode() {
return false;
}
-
+
/**
* Indicates whether this module must be called with a POST request
*/
@@ -694,7 +739,7 @@ abstract class ApiBase {
/**
* When modules crash, sometimes it is needed to do a profileOut() regardless
- * of the profiling state the module was in. This method does such cleanup.
+ * of the profiling state the module was in. This method does such cleanup.
*/
public function safeProfileOut() {
if ($this->mTimeIn !== 0) {
@@ -755,7 +800,7 @@ abstract class ApiBase {
ApiBase :: dieDebug(__METHOD__, 'called without calling profileDBOut() first');
return $this->mDBTime;
}
-
+
public static function debugPrint($value, $name = 'unknown', $backtrace = false) {
print "\n\n<pre><b>Debuging value '$name':</b>\n\n";
var_export($value);
@@ -769,7 +814,6 @@ abstract class ApiBase {
* Returns a String that identifies the version of this class.
*/
public static function getBaseVersion() {
- return __CLASS__ . ': $Id: ApiBase.php 31259 2008-02-25 14:14:55Z catrope $';
- }
+ return __CLASS__ . ': $Id: ApiBase.php 36309 2008-06-15 20:37:28Z catrope $';
+ }
}
-
diff --git a/includes/api/ApiBlock.php b/includes/api/ApiBlock.php
index e5c238ae..34813bf7 100644
--- a/includes/api/ApiBlock.php
+++ b/includes/api/ApiBlock.php
@@ -31,7 +31,7 @@ if (!defined('MEDIAWIKI')) {
* API module that facilitates the blocking of users. Requires API write mode
* to be enabled.
*
- * @addtogroup API
+ * @ingroup API
*/
class ApiBlock extends ApiBase {
@@ -87,17 +87,15 @@ class ApiBlock extends ApiBase {
$form->BlockEmail = $params['noemail'];
$form->BlockHideName = $params['hidename'];
- $dbw = wfGetDb(DB_MASTER);
- $dbw->begin();
+ $userID = $expiry = null;
$retval = $form->doBlock($userID, $expiry);
if(!empty($retval))
// We don't care about multiple errors, just report one of them
$this->dieUsageMsg($retval);
- $dbw->commit();
$res['user'] = $params['user'];
$res['userID'] = $userID;
- $res['expiry'] = ($expiry == Block::infinity() ? 'infinite' : $expiry);
+ $res['expiry'] = ($expiry == Block::infinity() ? 'infinite' : wfTimestamp(TS_ISO_8601, $expiry));
$res['reason'] = $params['reason'];
if($params['anononly'])
$res['anononly'] = '';
@@ -159,6 +157,6 @@ class ApiBlock extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiBlock.php 30222 2008-01-28 19:05:26Z catrope $';
+ return __CLASS__ . ': $Id: ApiBlock.php 35388 2008-05-27 10:18:28Z catrope $';
}
}
diff --git a/includes/api/ApiDelete.php b/includes/api/ApiDelete.php
index cd747e7e..06592d46 100644
--- a/includes/api/ApiDelete.php
+++ b/includes/api/ApiDelete.php
@@ -29,10 +29,10 @@ if (!defined('MEDIAWIKI')) {
/**
- * API module that facilitates deleting pages. The API eqivalent of action=delete.
+ * API module that facilitates deleting pages. The API eqivalent of action=delete.
* Requires API write mode to be enabled.
*
- * @addtogroup API
+ * @ingroup API
*/
class ApiDelete extends ApiBase {
@@ -42,16 +42,16 @@ class ApiDelete extends ApiBase {
/**
* Extracts the title, token, and reason from the request parameters and invokes
- * the local delete() function with these as arguments. It does not make use of
- * the delete function specified by Article.php. If the deletion succeeds, the
- * details of the article deleted and the reason for deletion are added to the
+ * the local delete() function with these as arguments. It does not make use of
+ * the delete function specified by Article.php. If the deletion succeeds, the
+ * details of the article deleted and the reason for deletion are added to the
* result object.
*/
public function execute() {
global $wgUser;
$this->getMain()->requestWriteMode();
$params = $this->extractRequestParams();
-
+
$titleObj = NULL;
if(!isset($params['title']))
$this->dieUsageMsg(array('missingparam', 'title'));
@@ -64,21 +64,45 @@ class ApiDelete extends ApiBase {
if(!$titleObj->exists())
$this->dieUsageMsg(array('notanarticle'));
- $articleObj = new Article($titleObj);
$reason = (isset($params['reason']) ? $params['reason'] : NULL);
- $dbw = wfGetDb(DB_MASTER);
- $dbw->begin();
- $retval = self::delete($articleObj, $params['token'], $reason);
-
- if(!empty($retval))
- // We don't care about multiple errors, just report one of them
- $this->dieUsageMsg(current($retval));
+ if ($titleObj->getNamespace() == NS_IMAGE) {
+ $retval = self::deletefile($params['token'], $titleObj, $params['oldimage'], $reason, false);
+ if(!empty($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))
+ // We don't care about multiple errors, just report one of them
+ $this->dieUsageMsg(current($retval));
+
+ if($params['watch'] || $wgUser->getOption('watchdeletion'))
+ $articleObj->doWatch();
+ else if($params['unwatch'])
+ $articleObj->doUnwatch();
+ }
- $dbw->commit();
$r = array('title' => $titleObj->getPrefixedText(), 'reason' => $reason);
$this->getResult()->addValue(null, $this->getModuleName(), $r);
}
+ private static function getPermissionsError(&$title, $token) {
+ global $wgUser;
+ // Check wiki readonly
+ if (wfReadOnly()) return array(array('readonlytext'));
+
+ // Check permissions
+ $errors = $title->getUserPermissionsErrors('delete', $wgUser);
+ if (count($errors) > 0) return $errors;
+
+ // Check token
+ if(!$wgUser->matchEditToken($token))
+ return array(array('sessionfailure'));
+ return array();
+ }
+
/**
* We have our own delete() function, since Article.php's implementation is split in two phases
*
@@ -90,41 +114,67 @@ class ApiDelete extends ApiBase {
public static function delete(&$article, $token, &$reason = NULL)
{
global $wgUser;
-
- // Check permissions
- $errors = $article->mTitle->getUserPermissionsErrors('delete', $wgUser);
- if(!empty($errors))
- return $errors;
- if(wfReadOnly())
- return array(array('readonlytext'));
- if($wgUser->isBlocked())
- return array(array('blocked'));
-
- // Check token
- if(!$wgUser->matchEditToken($token))
- return array(array('sessionfailure'));
+
+ $errors = self::getPermissionsError($article->getTitle(), $token);
+ if (count($errors)) return $errors;
// Auto-generate a summary, if necessary
if(is_null($reason))
{
+ # Need to pass a throwaway variable because generateReason expects
+ # a reference
+ $hasHistory = false;
$reason = $article->generateReason($hasHistory);
if($reason === false)
return array(array('cannotdelete'));
}
+
+ if (!wfRunHooks('ArticleDelete', array(&$article, &$wgUser, &$reason)))
+ $this->dieUsageMsg(array('hookaborted'));
// Luckily, Article.php provides a reusable delete function that does the hard work for us
- if($article->doDeleteArticle($reason))
+ if($article->doDeleteArticle($reason)) {
+ wfRunHooks('ArticleDeleteComplete', array(&$article, &$wgUser, $reason, $article->getId()));
return array();
+ }
return array(array('cannotdelete', $article->mTitle->getPrefixedText()));
}
+
+ public static function deleteFile($token, &$title, $oldimage, &$reason = NULL, $suppress = false)
+ {
+ $errors = self::getPermissionsError($title, $token);
+ if (count($errors)) return $errors;
+
+ if( $oldimage && !FileDeleteForm::isValidOldSpec($oldimage) )
+ return array(array('invalidoldimage'));
+
+ $file = wfFindFile($title, false, FileRepo::FIND_IGNORE_REDIRECT);
+ $oldfile = false;
+
+ if( $oldimage )
+ $oldfile = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $title, $oldimage );
+
+ if( !FileDeleteForm::haveDeletableFile($file, $oldfile, $oldimage) )
+ return array(array('nofile'));
+
+ $status = FileDeleteForm::doDelete( $title, $file, $oldimage, $reason, $suppress );
+
+ if( !$status->isGood() )
+ return array(array('cannotdelete', $title->getPrefixedText()));
+
+ return array();
+ }
public function mustBePosted() { return true; }
-
+
public function getAllowedParams() {
return array (
'title' => null,
'token' => null,
'reason' => null,
+ 'watch' => false,
+ 'unwatch' => false,
+ 'oldimage' => null
);
}
@@ -132,7 +182,10 @@ class ApiDelete extends ApiBase {
return array (
'title' => 'Title of the page you want to delete.',
'token' => 'A delete token previously retrieved through prop=info',
- 'reason' => 'Reason for the deletion. If not set, an automatically generated reason will be used.'
+ 'reason' => 'Reason for the deletion. If not set, an automatically generated reason will be used.',
+ 'watch' => 'Add the page to your watchlist',
+ 'unwatch' => 'Remove the page from your watchlist',
+ 'oldimage' => 'The name of the old image to delete as provided by iiprop=archivename'
);
}
@@ -150,6 +203,6 @@ class ApiDelete extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiDelete.php 30222 2008-01-28 19:05:26Z catrope $';
+ return __CLASS__ . ': $Id: ApiDelete.php 35350 2008-05-26 12:15:21Z simetrical $';
}
}
diff --git a/includes/api/ApiEditPage.php b/includes/api/ApiEditPage.php
new file mode 100644
index 00000000..d10432f3
--- /dev/null
+++ b/includes/api/ApiEditPage.php
@@ -0,0 +1,299 @@
+<?php
+
+/*
+ * Created on August 16, 2007
+ *
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2007 Iker Labarga <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");
+}
+
+/**
+ * A query module to list all external URLs found on a given set of pages.
+ *
+ * @ingroup API
+ */
+class ApiEditPage extends ApiBase {
+
+ public function __construct($query, $moduleName) {
+ parent :: __construct($query, $moduleName);
+ }
+
+ public function execute() {
+ global $wgUser;
+ $this->getMain()->requestWriteMode();
+
+ $params = $this->extractRequestParams();
+ if(is_null($params['title']))
+ $this->dieUsageMsg(array('missingparam', 'title'));
+ if(is_null($params['text']) && is_null($params['appendtext']) && is_null($params['prependtext']))
+ $this->dieUsageMsg(array('missingtext'));
+ if(is_null($params['token']))
+ $this->dieUsageMsg(array('missingparam', 'token'));
+ if(!$wgUser->matchEditToken($params['token']))
+ $this->dieUsageMsg(array('sessionfailure'));
+
+ $titleObj = Title::newFromText($params['title']);
+ if(!$titleObj)
+ $this->dieUsageMsg(array('invalidtitle', $params['title']));
+
+ if($params['createonly'] && $titleObj->exists())
+ $this->dieUsageMsg(array('createonly-exists'));
+ if($params['nocreate'] && !$titleObj->exists())
+ $this->dieUsageMsg(array('nocreate-missing'));
+
+ // Now let's check whether we're even allowed to do this
+ $errors = $titleObj->getUserPermissionsErrors('edit', $wgUser);
+ if(!$titleObj->exists())
+ $errors = array_merge($errors, $titleObj->getUserPermissionsErrors('create', $wgUser));
+ if(!empty($errors))
+ $this->dieUsageMsg($errors[0]);
+
+ $articleObj = new Article($titleObj);
+ $toMD5 = $params['text'];
+ if(!is_null($params['appendtext']) || !is_null($params['prependtext']))
+ {
+ $content = $articleObj->getContent();
+ $params['text'] = $params['prependtext'] . $content . $params['appendtext'];
+ $toMD5 = $params['prependtext'] . $params['appendtext'];
+ }
+
+ # See if the MD5 hash checks out
+ if(isset($params['md5']))
+ if(md5($toMD5) !== $params['md5'])
+ $this->dieUsageMsg(array('hashcheckfailed'));
+
+ $ep = new EditPage($articleObj);
+ // EditPage wants to parse its stuff from a WebRequest
+ // That interface kind of sucks, but it's workable
+ $reqArr = array('wpTextbox1' => $params['text'],
+ 'wpEdittoken' => $params['token'],
+ 'wpIgnoreBlankSummary' => ''
+ );
+ if(!is_null($params['summary']))
+ $reqArr['wpSummary'] = $params['summary'];
+ # Watch out for basetimestamp == ''
+ # wfTimestamp() treats it as NOW, almost certainly causing an edit conflict
+ if(!is_null($params['basetimestamp']) && $params['basetimestamp'] != '')
+ $reqArr['wpEdittime'] = wfTimestamp(TS_MW, $params['basetimestamp']);
+ else
+ $reqArr['wpEdittime'] = $articleObj->getTimestamp();
+ # Fake wpStartime
+ $reqArr['wpStarttime'] = $reqArr['wpEdittime'];
+ if($params['minor'] || (!$params['notminor'] && $wgUser->getOption('minordefault')))
+ $reqArr['wpMinoredit'] = '';
+ if($params['recreate'])
+ $reqArr['wpRecreate'] = '';
+ if(!is_null($params['section']))
+ {
+ $section = intval($params['section']);
+ if($section == 0 && $params['section'] != '0' && $params['section'] != 'new')
+ $this->dieUsage("The section parameter must be set to an integer or 'new'", "invalidsection");
+ $reqArr['wpSection'] = $params['section'];
+ }
+
+ if($params['watch'])
+ $watch = true;
+ else if($params['unwatch'])
+ $watch = false;
+ else if($titleObj->userIsWatching())
+ $watch = true;
+ else if($wgUser->getOption('watchdefault'))
+ $watch = true;
+ else if($wgUser->getOption('watchcreations') && !$titleObj->exists())
+ $watch = true;
+ else
+ $watch = false;
+ if($watch)
+ $reqArr['wpWatchthis'] = '';
+
+ $req = new FauxRequest($reqArr, true);
+ $ep->importFormData($req);
+
+ # Run hooks
+ # Handle CAPTCHA parameters
+ global $wgRequest;
+ if(isset($params['captchaid']))
+ $wgRequest->data['wpCaptchaId'] = $params['captchaid'];
+ if(isset($params['captchaword']))
+ $wgRequest->data['wpCaptchaWord'] = $params['captchaword'];
+ $r = array();
+ if(!wfRunHooks('APIEditBeforeSave', array(&$ep, $ep->textbox1, &$r)))
+ {
+ if(!empty($r))
+ {
+ $r['result'] = "Failure";
+ $this->getResult()->addValue(null, $this->getModuleName(), $r);
+ return;
+ }
+ else
+ $this->dieUsageMsg(array('hookaborted'));
+ }
+
+ # Do the actual save
+ $oldRevId = $articleObj->getRevIdFetched();
+ $result = null;
+ # *Something* is setting $wgTitle to a title corresponding to "Msg",
+ # but that breaks API mode detection through is_null($wgTitle)
+ global $wgTitle;
+ $wgTitle = null;
+ # Fake $wgRequest for some hooks inside EditPage
+ # FIXME: This interface SUCKS
+ $oldRequest = $wgRequest;
+ $wgRequest = $req;
+
+ $retval = $ep->internalAttemptSave($result, $wgUser->isAllowed('bot') && $params['bot']);
+ $wgRequest = $oldRequest;
+ switch($retval)
+ {
+ case EditPage::AS_HOOK_ERROR:
+ case EditPage::AS_HOOK_ERROR_EXPECTED:
+ $this->dieUsageMsg(array('hookaborted'));
+ case EditPage::AS_IMAGE_REDIRECT_ANON:
+ $this->dieUsageMsg(array('noimageredirect-anon'));
+ case EditPage::AS_IMAGE_REDIRECT_LOGGED:
+ $this->dieUsageMsg(array('noimageredirect-logged'));
+ case EditPage::AS_SPAM_ERROR:
+ $this->dieUsageMsg(array('spamdetected', $result['spam']));
+ case EditPage::AS_FILTERING:
+ $this->dieUsageMsg(array('filtered'));
+ case EditPage::AS_BLOCKED_PAGE_FOR_USER:
+ $this->dieUsageMsg(array('blockedtext'));
+ case EditPage::AS_MAX_ARTICLE_SIZE_EXCEEDED:
+ case EditPage::AS_CONTENT_TOO_BIG:
+ global $wgMaxArticleSize;
+ $this->dieUsageMsg(array('contenttoobig', $wgMaxArticleSize));
+ case EditPage::AS_READ_ONLY_PAGE_ANON:
+ $this->dieUsageMsg(array('noedit-anon'));
+ case EditPage::AS_READ_ONLY_PAGE_LOGGED:
+ $this->dieUsageMsg(array('noedit'));
+ case EditPage::AS_READ_ONLY_PAGE:
+ $this->dieUsageMsg(array('readonlytext'));
+ case EditPage::AS_RATE_LIMITED:
+ $this->dieUsageMsg(array('actionthrottledtext'));
+ case EditPage::AS_ARTICLE_WAS_DELETED:
+ $this->dieUsageMsg(array('wasdeleted'));
+ case EditPage::AS_NO_CREATE_PERMISSION:
+ $this->dieUsageMsg(array('nocreate-loggedin'));
+ case EditPage::AS_BLANK_ARTICLE:
+ $this->dieUsageMsg(array('blankpage'));
+ 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_END:
+ # This usually means some kind of race condition
+ # or DB weirdness occurred. Throw an unknown error here.
+ $this->dieUsageMsg(array('unknownerror', 'AS_END'));
+ 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();
+ if($newRevId == $oldRevId)
+ $r['nochange'] = '';
+ else
+ {
+ $r['oldrevid'] = $oldRevId;
+ $r['newrevid'] = $newRevId;
+ }
+ break;
+ default:
+ $this->dieUsageMsg(array('unknownerror', $retval));
+ }
+ $this->getResult()->addValue(null, $this->getModuleName(), $r);
+ }
+
+ public function mustBePosted() {
+ return true;
+ }
+
+ protected function getDescription() {
+ return 'Create and edit pages.';
+ }
+
+ protected function getAllowedParams() {
+ return array (
+ 'title' => null,
+ 'section' => null,
+ 'text' => null,
+ 'token' => null,
+ 'summary' => null,
+ 'minor' => false,
+ 'notminor' => false,
+ 'bot' => false,
+ 'basetimestamp' => null,
+ 'recreate' => false,
+ 'createonly' => false,
+ 'nocreate' => false,
+ 'captchaword' => null,
+ 'captchaid' => null,
+ 'watch' => false,
+ 'unwatch' => false,
+ 'md5' => null,
+ 'prependtext' => null,
+ 'appendtext' => null,
+ );
+ }
+
+ protected function getParamDescription() {
+ return array (
+ 'title' => 'Page title',
+ 'section' => 'Section number. 0 for the top section, \'new\' for a new section',
+ 'text' => 'Page content',
+ 'token' => 'Edit token. You can get one of these through prop=info',
+ 'summary' => 'Edit summary. Also section title when section=new',
+ 'minor' => 'Minor edit',
+ 'notminor' => 'Non-minor edit',
+ 'bot' => 'Mark this edit as bot',
+ 'basetimestamp' => array('Timestamp of the base revision (gotten through prop=revisions&rvprop=timestamp).',
+ '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',
+ 'watch' => 'Add the page to your watchlist',
+ 'unwatch' => 'Remove the page from your watchlist',
+ 'captchaid' => 'CAPTCHA ID from previous request',
+ 'captchaword' => 'Answer to the CAPTCHA',
+ 'md5' => array( 'The MD5 hash of the text parameter, or the prependtext and appendtext parameters concatenated.',
+ 'If set, the edit won\'t be done unless the hash is correct'),
+ 'prependtext' => array( 'Add this text to the beginning of the page. Overrides text.',
+ 'Don\'t use together with section: that won\'t do what you expect.'),
+ 'appendtext' => 'Add this text to the end of the page. Overrides text',
+ );
+ }
+
+ protected function getExamples() {
+ return array (
+ "Edit a page (anonymous user):",
+ " api.php?action=edit&title=Test&summary=test%20summary&text=article%20content&basetimestamp=20070824123454&token=%2B\\"
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiEditPage.php 36309 2008-06-15 20:37:28Z catrope $';
+ }
+}
diff --git a/includes/api/ApiEmailUser.php b/includes/api/ApiEmailUser.php
new file mode 100644
index 00000000..7e083536
--- /dev/null
+++ b/includes/api/ApiEmailUser.php
@@ -0,0 +1,114 @@
+<?php
+
+/*
+ * Created on June 1, 2008
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2008 Bryan Tong Minh <Bryan.TongMinh@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");
+}
+
+
+/**
+ * @ingroup API
+ */
+class ApiEmailUser extends ApiBase {
+
+ public function __construct($main, $action) {
+ parent :: __construct($main, $action);
+ }
+
+ public function execute() {
+ global $wgUser;
+ $this->getMain()->requestWriteMode();
+ $params = $this->extractRequestParams();
+
+ // Check required parameters
+ if ( !isset( $params['target'] ) )
+ $this->dieUsageMsg( array( 'missingparam', 'target' ) );
+ if ( !isset( $params['text'] ) )
+ $this->dieUsageMsg( array( 'missingparam', 'text' ) );
+ if ( !isset( $params['token'] ) )
+ $this->dieUsageMsg( array( 'missingparam', 'token' ) );
+
+ // Validate target
+ $targetUser = EmailUserForm::validateEmailTarget( $params['target'] );
+ if ( !( $targetUser instanceof User ) )
+ $this->dieUsageMsg( array( $targetUser[0] ) );
+
+ // Check permissions
+ $error = EmailUserForm::getPermissionsError( $wgUser, $params['token'] );
+ if ( $error )
+ $this->dieUsageMsg( array( $error[0] ) );
+
+
+ $form = new EmailUserForm( $targetUser, $params['text'], $params['subject'], $params['ccme'] );
+ $retval = $form->doSubmit();
+ if ( is_null( $retval ) )
+ $result = array( 'result' => 'Success' );
+ else
+ $result = array( 'result' => 'Failure',
+ 'message' => $retval->getMessage() );
+
+ $this->getResult()->addValue( null, $this->getModuleName(), $result );
+ }
+
+ public function mustBePosted() { return true; }
+
+ public function getAllowedParams() {
+ return array (
+ 'target' => null,
+ 'subject' => null,
+ 'text' => null,
+ 'token' => null,
+ 'ccme' => false,
+ );
+ }
+
+ public function getParamDescription() {
+ return array (
+ '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',
+ );
+ }
+
+ public function getDescription() {
+ return array(
+ 'Emails a user.'
+ );
+ }
+
+ protected function getExamples() {
+ return array (
+ 'api.php?action=emailuser&target=WikiSysop&text=Content'
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: $';
+ }
+}
+ \ No newline at end of file
diff --git a/includes/api/ApiExpandTemplates.php b/includes/api/ApiExpandTemplates.php
index 278896fa..397aece3 100644
--- a/includes/api/ApiExpandTemplates.php
+++ b/includes/api/ApiExpandTemplates.php
@@ -33,7 +33,7 @@ if (!defined('MEDIAWIKI')) {
* any templates in a provided string, and returns the result of this expansion
* to the caller.
*
- * @addtogroup API
+ * @ingroup API
*/
class ApiExpandTemplates extends ApiBase {
@@ -43,22 +43,35 @@ class ApiExpandTemplates extends ApiBase {
public function execute() {
// Get parameters
- $params = $this->extractRequestParams();
- $text = $params['text'];
- $title = $params['title'];
+ extract( $this->extractRequestParams() );
$retval = '';
//Create title for parser
- $title_obj = Title :: newFromText($params['title']);
+ $title_obj = Title :: newFromText( $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 title is "API". For example, ExpandTemplates uses "ExpendTemplates" for it
+
+ $result = $this->getResult();
// Parse text
global $wgParser;
- $retval = $wgParser->preprocess( $text, $title_obj, new ParserOptions() );
+ $options = new ParserOptions();
+ if ( $generatexml )
+ {
+ $wgParser->startExternalParse( $title_obj, $options, OT_PREPROCESS );
+ $dom = $wgParser->preprocessToDom( $text );
+ if ( is_callable( array( $dom, 'saveXML' ) ) ) {
+ $xml = $dom->saveXML();
+ } else {
+ $xml = $dom->__toString();
+ }
+ $xml_result = array();
+ $result->setContent( $xml_result, $xml );
+ $result->addValue( null, 'parsetree', $xml_result);
+ }
+ $retval = $wgParser->preprocess( $text, $title_obj, $options );
// Return result
- $result = $this->getResult();
$retval_array = array();
$result->setContent( $retval_array, $retval );
$result->addValue( null, $this->getModuleName(), $retval_array );
@@ -66,10 +79,11 @@ class ApiExpandTemplates extends ApiBase {
public function getAllowedParams() {
return array (
- 'title' => array(
+ 'title' => array(
ApiBase :: PARAM_DFLT => 'API',
),
- 'text' => null
+ 'text' => null,
+ 'generatexml' => false,
);
}
@@ -77,6 +91,7 @@ class ApiExpandTemplates extends ApiBase {
return array (
'text' => 'Wikitext to convert',
'title' => 'Title of page',
+ 'generatexml' => 'Generate XML parse tree',
);
}
@@ -91,7 +106,6 @@ class ApiExpandTemplates extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiExpandTemplates.php 30222 2008-01-28 19:05:26Z catrope $';
+ return __CLASS__ . ': $Id: ApiExpandTemplates.php 35098 2008-05-20 17:13:28Z ialex $';
}
}
-
diff --git a/includes/api/ApiFeedWatchlist.php b/includes/api/ApiFeedWatchlist.php
index 9b17b9d3..109b6552 100644
--- a/includes/api/ApiFeedWatchlist.php
+++ b/includes/api/ApiFeedWatchlist.php
@@ -32,8 +32,8 @@ if (!defined('MEDIAWIKI')) {
* This action allows users to get their watchlist items in RSS/Atom formats.
* When executed, it performs a nested call to the API to get the needed data,
* and formats it in a proper format.
- *
- * @addtogroup API
+ *
+ * @ingroup API
*/
class ApiFeedWatchlist extends ApiBase {
@@ -53,15 +53,15 @@ class ApiFeedWatchlist extends ApiBase {
* Wrap the result as an RSS/Atom feed.
*/
public function execute() {
-
- global $wgFeedClasses, $wgSitename, $wgContLanguageCode;
+
+ global $wgFeedClasses, $wgFeedLimit, $wgSitename, $wgContLanguageCode;
try {
$params = $this->extractRequestParams();
-
+
// limit to the number of hours going from now back
$endTime = wfTimestamp(TS_MW, time() - intval($params['hours'] * 60 * 60));
-
+
$dbr = wfGetDB( DB_SLAVE );
// Prepare parameters for nested request
$fauxReqArr = array (
@@ -72,7 +72,7 @@ class ApiFeedWatchlist extends ApiBase {
'wlprop' => 'title|user|comment|timestamp',
'wldir' => 'older', // reverse order - from newest to oldest
'wlend' => $dbr->timestamp($endTime), // stop at this time
- 'wllimit' => 50
+ 'wllimit' => (50 > $wgFeedLimit) ? $wgFeedLimit : 50
);
// Check for 'allrev' parameter, and if found, show all revisions to each page on wl.
@@ -80,35 +80,35 @@ class ApiFeedWatchlist extends ApiBase {
// Create the request
$fauxReq = new FauxRequest ( $fauxReqArr );
-
+
// Execute
$module = new ApiMain($fauxReq);
$module->execute();
// Get data array
$data = $module->getResultData();
-
+
$feedItems = array ();
foreach ($data['query']['watchlist'] as $info) {
$feedItems[] = $this->createFeedItem($info);
}
-
+
$feedTitle = $wgSitename . ' - ' . wfMsgForContent('watchlist') . ' [' . $wgContLanguageCode . ']';
$feedUrl = SpecialPage::getTitleFor( 'Watchlist' )->getFullUrl();
-
+
$feed = new $wgFeedClasses[$params['feedformat']] ($feedTitle, htmlspecialchars(wfMsgForContent('watchlist')), $feedUrl);
-
+
ApiFormatFeedWrapper :: setResult($this->getResult(), $feed, $feedItems);
} catch (Exception $e) {
// Error results should not be cached
$this->getMain()->setCacheMaxAge(0);
-
+
$feedTitle = $wgSitename . ' - Error - ' . wfMsgForContent('watchlist') . ' [' . $wgContLanguageCode . ']';
$feedUrl = SpecialPage::getTitleFor( 'Watchlist' )->getFullUrl();
-
- $feedFormat = isset($params['feedformat']) ? $params['feedformat'] : 'rss';
+
+ $feedFormat = isset($params['feedformat']) ? $params['feedformat'] : 'rss';
$feed = new $wgFeedClasses[$feedFormat] ($feedTitle, htmlspecialchars(wfMsgForContent('watchlist')), $feedUrl);
@@ -175,7 +175,6 @@ class ApiFeedWatchlist extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFeedWatchlist.php 30222 2008-01-28 19:05:26Z catrope $';
+ return __CLASS__ . ': $Id: ApiFeedWatchlist.php 35098 2008-05-20 17:13:28Z ialex $';
}
}
-
diff --git a/includes/api/ApiFormatBase.php b/includes/api/ApiFormatBase.php
index 768a18ac..db58fe52 100644
--- a/includes/api/ApiFormatBase.php
+++ b/includes/api/ApiFormatBase.php
@@ -30,12 +30,12 @@ if (!defined('MEDIAWIKI')) {
/**
* This is the abstract base class for API formatters.
- *
- * @addtogroup API
+ *
+ * @ingroup API
*/
abstract class ApiFormatBase extends ApiBase {
- private $mIsHtml, $mFormat, $mUnescapeAmps, $mHelp;
+ private $mIsHtml, $mFormat, $mUnescapeAmps, $mHelp, $mCleared;
/**
* Create a new instance of the formatter.
@@ -50,6 +50,7 @@ abstract class ApiFormatBase extends ApiBase {
else
$this->mFormat = $format;
$this->mFormat = strtoupper($this->mFormat);
+ $this->mCleared = false;
}
/**
@@ -62,7 +63,7 @@ abstract class ApiFormatBase extends ApiBase {
/**
* If formatter outputs data results as is, the results must first be sanitized.
* An XML formatter on the other hand uses special tags, such as "_element" for special handling,
- * and thus needs to override this function to return true.
+ * and thus needs to override this function to return true.
*/
public function getNeedsRawData() {
return false;
@@ -82,8 +83,8 @@ abstract class ApiFormatBase extends ApiBase {
/**
* Returns true when an HTML filtering printer should be used.
- * The default implementation assumes that formats ending with 'fm'
- * should be formatted in HTML.
+ * The default implementation assumes that formats ending with 'fm'
+ * should be formatted in HTML.
*/
public function getIsHtml() {
return $this->mIsHtml;
@@ -111,7 +112,7 @@ abstract class ApiFormatBase extends ApiBase {
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
-<?php if ($this->mUnescapeAmps) {
+<?php if ($this->mUnescapeAmps) {
?> <title>MediaWiki API</title>
<?php } else {
?> <title>MediaWiki API Result</title>
@@ -127,7 +128,7 @@ abstract class ApiFormatBase extends ApiBase {
<small>
You are looking at the HTML representation of the <?php echo( $this->mFormat ); ?> format.<br/>
HTML is good for debugging, but probably is not suitable for your application.<br/>
-See <a href='http://www.mediawiki.org/wiki/API'>complete documentation</a>, or
+See <a href='http://www.mediawiki.org/wiki/API'>complete documentation</a>, or
<a href='<?php echo( $script ); ?>'>API help</a> for more information.
</small>
<?php
@@ -166,7 +167,17 @@ See <a href='http://www.mediawiki.org/wiki/API'>complete documentation</a>, or
if ($this->getIsHtml())
echo $this->formatHTML($text);
else
+ {
+ // For non-HTML output, clear all errors that might have been
+ // displayed if display_errors=On
+ // Do this only once, of course
+ if(!$this->mCleared)
+ {
+ ob_clean();
+ $this->mCleared = true;
+ }
echo $text;
+ }
}
/**
@@ -175,7 +186,7 @@ See <a href='http://www.mediawiki.org/wiki/API'>complete documentation</a>, or
public function setHelp( $help = true ) {
$this->mHelp = true;
}
-
+
/**
* Prety-print various elements in HTML format, such as xml tags and URLs.
* This method also replaces any '<' with &lt;
@@ -188,16 +199,17 @@ See <a href='http://www.mediawiki.org/wiki/API'>complete documentation</a>, or
$text = preg_replace('/\&lt;(!--.*?--|.*?)\&gt;/', '<span style="color:blue;">&lt;\1&gt;</span>', $text);
// identify URLs
$protos = "http|https|ftp|gopher";
- $text = ereg_replace("($protos)://[^ \\'\"()<\n]+", '<a href="\\0">\\0</a>', $text);
+ # 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);
// identify requests to api.php
- $text = ereg_replace("api\\.php\\?[^ \\()<\n\t]+", '<a href="\\0">\\0</a>', $text);
+ $text = preg_replace("#api\\.php\\?[^ \\()<\n\t]+#", '<a href="\\0">\\0</a>', $text);
if( $this->mHelp ) {
// make strings inside * bold
$text = ereg_replace("\\*[^<>\n]+\\*", '<b>\\0</b>', $text);
// make strings inside $ italic
$text = ereg_replace("\\$[^<>\n]+\\$", '<b><i>\\0</i></b>', $text);
}
-
+
/* Temporary fix for bad links in help messages. As a special case,
* XML-escaped metachars are de-escaped one level in the help message
* for legibility. Should be removed once we have completed a fully-html
@@ -220,13 +232,13 @@ See <a href='http://www.mediawiki.org/wiki/API'>complete documentation</a>, or
}
public static function getBaseVersion() {
- return __CLASS__ . ': $Id: ApiFormatBase.php 30222 2008-01-28 19:05:26Z catrope $';
+ return __CLASS__ . ': $Id: ApiFormatBase.php 36153 2008-06-10 15:20:22Z tstarling $';
}
}
/**
- * This printer is used to wrap an instance of the Feed class
- * @addtogroup API
+ * This printer is used to wrap an instance of the Feed class
+ * @ingroup API
*/
class ApiFormatFeedWrapper extends ApiFormatBase {
@@ -275,13 +287,12 @@ class ApiFormatFeedWrapper extends ApiFormatBase {
$feed->outItem($item);
$feed->outFooter();
} else {
- // Error has occured, print something usefull
- // TODO: make this error more informative using ApiBase :: dieDebug() or similar
- wfHttpError(500, 'Internal Server Error', '');
+ // Error has occured, print something useful
+ ApiBase::dieDebug( __METHOD__, 'Invalid feed class/item' );
}
}
-
+
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFormatBase.php 30222 2008-01-28 19:05:26Z catrope $';
+ return __CLASS__ . ': $Id: ApiFormatBase.php 36153 2008-06-10 15:20:22Z tstarling $';
}
}
diff --git a/includes/api/ApiFormatDbg.php b/includes/api/ApiFormatDbg.php
index f0fc5e91..254c140b 100644
--- a/includes/api/ApiFormatDbg.php
+++ b/includes/api/ApiFormatDbg.php
@@ -29,7 +29,7 @@ if (!defined('MEDIAWIKI')) {
}
/**
- * @addtogroup API
+ * @ingroup API
*/
class ApiFormatDbg extends ApiFormatBase {
@@ -53,7 +53,6 @@ class ApiFormatDbg extends ApiFormatBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFormatPhp.php 23531 2007-06-29 01:19:14Z simetrical $';
+ return __CLASS__ . ': $Id: ApiFormatDbg.php 35098 2008-05-20 17:13:28Z ialex $';
}
}
-
diff --git a/includes/api/ApiFormatJson.php b/includes/api/ApiFormatJson.php
index 852a64b6..42156849 100644
--- a/includes/api/ApiFormatJson.php
+++ b/includes/api/ApiFormatJson.php
@@ -29,7 +29,7 @@ if (!defined('MEDIAWIKI')) {
}
/**
- * @addtogroup API
+ * @ingroup API
*/
class ApiFormatJson extends ApiFormatBase {
@@ -54,7 +54,7 @@ class ApiFormatJson extends ApiFormatBase {
$params = $this->extractRequestParams();
$callback = $params['callback'];
if(!is_null($callback)) {
- $prefix = ereg_replace("[^_A-Za-z0-9]", "", $callback ) . "(";
+ $prefix = preg_replace("/[^][.\\'\\\"_A-Za-z0-9]/", "", $callback ) . "(";
$suffix = ")";
}
@@ -86,7 +86,6 @@ class ApiFormatJson extends ApiFormatBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFormatJson.php 31484 2008-03-03 05:46:20Z brion $';
+ return __CLASS__ . ': $Id: ApiFormatJson.php 35098 2008-05-20 17:13:28Z ialex $';
}
}
-
diff --git a/includes/api/ApiFormatJson_json.php b/includes/api/ApiFormatJson_json.php
index a8c649c3..87d7086e 100644
--- a/includes/api/ApiFormatJson_json.php
+++ b/includes/api/ApiFormatJson_json.php
@@ -45,12 +45,12 @@
* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
* DAMAGE.
*
-* @addtogroup API
+* @ingroup API
* @author Michal Migurski <mike-json@teczno.com>
* @author Matt Knapp <mdknapp[at]gmail[dot]com>
* @author Brett Stimmerman <brettstimmerman[at]gmail[dot]com>
* @copyright 2005 Michal Migurski
-* @version CVS: $Id: JSON.php,v 1.30 2006/03/08 16:10:20 migurski Exp $
+* @version CVS: $Id: ApiFormatJson_json.php 35098 2008-05-20 17:13:28Z ialex $
* @license http://www.opensource.org/licenses/bsd-license.php
* @see http://pear.php.net/pepr/pepr-proposal-show.php?id=198
*/
@@ -111,7 +111,7 @@ define('SERVICES_JSON_SUPPRESS_ERRORS', 32);
* $value = $json->decode($input);
* </code>
*
- * @addtogroup API
+ * @ingroup API
*/
class Services_JSON
{
@@ -257,7 +257,7 @@ class Services_JSON
*/
function encode2($var)
{
- if ($this->pretty) {
+ if ($this->pretty) {
$close = "\n" . str_repeat("\t", $this->indent);
$open = $close . "\t";
$mid = ',' . $open;
@@ -426,7 +426,7 @@ class Services_JSON
$this->indent++;
$elements = array_map(array($this, 'encode2'), $var);
$this->indent--;
-
+
foreach($elements as $element) {
if(Services_JSON::isError($element)) {
return $element;
@@ -703,7 +703,7 @@ class Services_JSON
// element in an associative array,
// for now
$parts = array();
-
+
if (preg_match('/^\s*(["\'].*[^\\\]["\'])\s*:\s*(\S.*),?$/Uis', $slice, $parts)) {
// "name":value pair
$key = $this->decode($parts[1]);
@@ -815,7 +815,7 @@ class Services_JSON
if (class_exists('PEAR_Error')) {
/**
- * @addtogroup API
+ * @ingroup API
*/
class Services_JSON_Error extends PEAR_Error
{
@@ -830,7 +830,7 @@ if (class_exists('PEAR_Error')) {
/**
* @todo Ultimately, this class shall be descended from PEAR_Error
- * @addtogroup API
+ * @ingroup API
*/
class Services_JSON_Error
{
@@ -840,7 +840,4 @@ if (class_exists('PEAR_Error')) {
}
}
-
}
-
-
diff --git a/includes/api/ApiFormatPhp.php b/includes/api/ApiFormatPhp.php
index f830d8e1..163d3028 100644
--- a/includes/api/ApiFormatPhp.php
+++ b/includes/api/ApiFormatPhp.php
@@ -29,7 +29,7 @@ if (!defined('MEDIAWIKI')) {
}
/**
- * @addtogroup API
+ * @ingroup API
*/
class ApiFormatPhp extends ApiFormatBase {
@@ -50,7 +50,6 @@ class ApiFormatPhp extends ApiFormatBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFormatPhp.php 30222 2008-01-28 19:05:26Z catrope $';
+ return __CLASS__ . ': $Id: ApiFormatPhp.php 35098 2008-05-20 17:13:28Z ialex $';
}
}
-
diff --git a/includes/api/ApiFormatTxt.php b/includes/api/ApiFormatTxt.php
index c4c45f68..5f608d5c 100644
--- a/includes/api/ApiFormatTxt.php
+++ b/includes/api/ApiFormatTxt.php
@@ -29,7 +29,7 @@ if (!defined('MEDIAWIKI')) {
}
/**
- * @addtogroup API
+ * @ingroup API
*/
class ApiFormatTxt extends ApiFormatBase {
@@ -53,7 +53,6 @@ class ApiFormatTxt extends ApiFormatBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFormatPhp.php 23531 2007-06-29 01:19:14Z simetrical $';
+ return __CLASS__ . ': $Id: ApiFormatTxt.php 35098 2008-05-20 17:13:28Z ialex $';
}
}
-
diff --git a/includes/api/ApiFormatWddx.php b/includes/api/ApiFormatWddx.php
index 22a0e482..0909539e 100644
--- a/includes/api/ApiFormatWddx.php
+++ b/includes/api/ApiFormatWddx.php
@@ -29,7 +29,7 @@ if (!defined('MEDIAWIKI')) {
}
/**
- * @addtogroup API
+ * @ingroup API
*/
class ApiFormatWddx extends ApiFormatBase {
@@ -85,7 +85,6 @@ class ApiFormatWddx extends ApiFormatBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFormatWddx.php 30222 2008-01-28 19:05:26Z catrope $';
+ return __CLASS__ . ': $Id: ApiFormatWddx.php 35098 2008-05-20 17:13:28Z ialex $';
}
}
-
diff --git a/includes/api/ApiFormatXml.php b/includes/api/ApiFormatXml.php
index d39e8049..d35eb3e9 100644
--- a/includes/api/ApiFormatXml.php
+++ b/includes/api/ApiFormatXml.php
@@ -29,11 +29,12 @@ if (!defined('MEDIAWIKI')) {
}
/**
- * @addtogroup API
+ * @ingroup API
*/
class ApiFormatXml extends ApiFormatBase {
private $mRootElemName = 'api';
+ private $mDoubleQuote = false;
public function __construct($main, $format) {
parent :: __construct($main, $format);
@@ -46,12 +47,15 @@ class ApiFormatXml extends ApiFormatBase {
public function getNeedsRawData() {
return true;
}
-
+
public function setRootElement($rootElemName) {
$this->mRootElemName = $rootElemName;
}
public function execute() {
+ $params = $this->extractRequestParams();
+ $this->mDoubleQuote = $params['xmldoublequote'];
+
$this->printText('<?xml version="1.0" encoding="utf-8"?>');
$this->recXmlPrint($this->mRootElemName, $this->getResultData(), $this->getIsHtml() ? -2 : null);
}
@@ -79,9 +83,10 @@ class ApiFormatXml extends ApiFormatBase {
switch (gettype($elemValue)) {
case 'array' :
-
if (isset ($elemValue['*'])) {
$subElemContent = $elemValue['*'];
+ if ($this->mDoubleQuote)
+ $subElemContent = $this->doubleQuote($subElemContent);
unset ($elemValue['*']);
} else {
$subElemContent = null;
@@ -97,6 +102,9 @@ class ApiFormatXml extends ApiFormatBase {
$indElements = array ();
$subElements = array ();
foreach ($elemValue as $subElemId => & $subElemValue) {
+ if (is_string($subElemValue) && $this->mDoubleQuote)
+ $subElemValue = $this->doubleQuote($subElemValue);
+
if (gettype($subElemId) === 'integer') {
$indElements[] = $subElemValue;
unset ($elemValue[$subElemId]);
@@ -136,12 +144,28 @@ class ApiFormatXml extends ApiFormatBase {
break;
}
}
+ private function doubleQuote( $text ) {
+ return Sanitizer::encodeAttribute( $text );
+ }
+
+ public function getAllowedParams() {
+ return array (
+ 'xmldoublequote' => false
+ );
+ }
+
+ public function getParamDescription() {
+ return array (
+ 'xmldoublequote' => 'If specified, double quotes all attributes and content.',
+ );
+ }
+
+
public function getDescription() {
return 'Output data in XML format' . parent :: getDescription();
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFormatXml.php 30222 2008-01-28 19:05:26Z catrope $';
+ return __CLASS__ . ': $Id: ApiFormatXml.php 37075 2008-07-04 22:44:57Z brion $';
}
}
-
diff --git a/includes/api/ApiFormatYaml.php b/includes/api/ApiFormatYaml.php
index 5e15aee6..cc255c63 100644
--- a/includes/api/ApiFormatYaml.php
+++ b/includes/api/ApiFormatYaml.php
@@ -29,7 +29,7 @@ if (!defined('MEDIAWIKI')) {
}
/**
- * @addtogroup API
+ * @ingroup API
*/
class ApiFormatYaml extends ApiFormatBase {
@@ -50,7 +50,6 @@ class ApiFormatYaml extends ApiFormatBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFormatYaml.php 30222 2008-01-28 19:05:26Z catrope $';
+ return __CLASS__ . ': $Id: ApiFormatYaml.php 35098 2008-05-20 17:13:28Z ialex $';
}
}
-
diff --git a/includes/api/ApiFormatYaml_spyc.php b/includes/api/ApiFormatYaml_spyc.php
index b2973b8c..c0d4093e 100644
--- a/includes/api/ApiFormatYaml_spyc.php
+++ b/includes/api/ApiFormatYaml_spyc.php
@@ -1,5 +1,5 @@
<?php
- /**
+ /**
* Spyc -- A Simple PHP YAML Class
* @version 0.2.3 -- 2006-02-04
* @author Chris Wanstrath <chris@ozmm.org>
@@ -8,29 +8,29 @@
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
- /**
+ /**
* A node, used by Spyc for parsing YAML.
- * @addtogroup API
+ * @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
*/
@@ -58,17 +58,17 @@
* $parser = new Spyc;
* $array = $parser->load($file);
* </code>
- * @addtogroup API
+ * @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
+ * 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:
+ * Usage:
* <code>
* $array = Spyc::YAMLLoad('lucky.yml');
* print_r($array);
@@ -81,7 +81,7 @@
$spyc = new Spyc;
return $spyc->load($input);
}
-
+
/**
* Dump YAML from PHP array statically
*
@@ -90,7 +90,7 @@
* 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
+ * 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
@@ -100,20 +100,20 @@
* @static
* @return string
* @param array $array PHP array
- * @param int $indent Pass in false to use the default, which is 2
+ * @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),
+ * 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:
+ * Usage:
* <code>
* $parser = new Spyc;
* $array = $parser->load('lucky.yml');
@@ -126,7 +126,7 @@
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)
+ if (!empty($input) && (strpos($input, "\n") === false)
&& file_exists($input)) {
$yaml = file($input);
} else {
@@ -139,7 +139,7 @@
$this->_lastNode = $base->id;
$this->_inBlock = false;
$this->_isInline = false;
-
+
foreach ($yaml as $linenum => $line) {
$ifchk = trim($line);
@@ -149,7 +149,7 @@
' with a tab. YAML only recognizes spaces. Please reformat.';
die($err);
}
-
+
if ($this->_inBlock === false && empty($ifchk)) {
continue;
} elseif ($this->_inBlock == true && empty($ifchk)) {
@@ -159,7 +159,7 @@
// 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
@@ -172,16 +172,16 @@
$node->parent = $this->_allNodes[$this->_lastNode]->parent;
}
}
- } elseif ($this->_lastIndent < $node->indent) {
+ } 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
+
+ // 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;
@@ -190,7 +190,7 @@
if ($chk === '>') {
$this->_inBlock = true;
$this->_blockEnd = ' ';
- $parent->data[key($parent->data)] =
+ $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;
@@ -198,7 +198,7 @@
} elseif ($chk === '|') {
$this->_inBlock = true;
$this->_blockEnd = "\n";
- $parent->data[key($parent->data)] =
+ $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;
@@ -212,11 +212,11 @@
$this->_inBlock = false;
if ($this->_blockEnd = "\n") {
$last =& $this->_allNodes[$this->_lastNode];
- $last->data[key($last->data)] =
+ $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) {
@@ -225,7 +225,7 @@
}
}
}
-
+
if ($this->_inBlock === false) {
// Set these properties with information from our current node
$this->_lastIndent = $node->indent;
@@ -239,13 +239,13 @@
$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 (
+ 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)]))
+ ||
(preg_match('/^\*([^ ]+)/',$node->data[key($node->data)])) )
) {
$this->_haveRefs[] =& $this->_allNodes[$node->id];
@@ -256,9 +256,9 @@
) {
// Incomplete reference making code. Ugly, needs cleaned up.
foreach ($node->data[key($node->data)] as $d) {
- if ( !is_array($d) &&
- ( (preg_match('/^&([^ ]+)/',$d))
- ||
+ if ( !is_array($d) &&
+ ( (preg_match('/^&([^ ]+)/',$d))
+ ||
(preg_match('/^\*([^ ]+)/',$d)) )
) {
$this->_haveRefs[] =& $this->_allNodes[$node->id];
@@ -269,15 +269,15 @@
}
}
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
*
@@ -286,7 +286,7 @@
* 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
+ * 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
@@ -295,7 +295,7 @@
* @access public
* @return string
* @param array $array PHP array
- * @param int $indent Pass in false to use the default, which is 2
+ * @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) {
@@ -308,29 +308,29 @@
} 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;
@@ -342,7 +342,7 @@
/**#@-*/
/**** Private Methods ****/
-
+
/**
* Attempts to convert a key / value array item to YAML
* @access private
@@ -350,7 +350,7 @@
* @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?
@@ -366,14 +366,14 @@
}
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 = '';
@@ -395,9 +395,15 @@
function _needLiteral($value) {
# Check whether the string contains # or : or begins with any of:
# [ - ? , [ ] { } ! * & | > ' " % @ ` ]
- return (bool)(preg_match("/[#:]/", $value) || preg_match("/^[-?,[\]{}!*&|>'\"%@`]/", $value));
+ # 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
@@ -405,23 +411,29 @@
* @param $key The name of the key
* @param $value The value of the item
* @param $indent The indent of the current node
- */
+ */
function _dumpNode($key,$value,$indent) {
// do some folding here, for blocks
- if (strpos($value,"\n") || $this->_needLiteral($value)) {
+ if ($this->_needLiteral($value)) {
$value = $this->_doLiteralBlock($value,$indent);
- } else {
+ } else {
$value = $this->_doFolding($value,$indent);
}
-
+
$spaces = str_repeat(' ',$indent);
if (is_int($key)) {
// It's a sequence
- $string = $spaces.'- '.$value."\n";
+ if ($value)
+ $string = $spaces.'- '.$value."\n";
+ else
+ $string = $spaces . "-\n";
} else {
- // It's mapped
- $string = $spaces.$key.': '.$value."\n";
+ // It's mapped
+ if ($value)
+ $string = $spaces.$key.': '.$value."\n";
+ else
+ $string = $spaces . $key . ":\n";
}
return $string;
}
@@ -430,9 +442,9 @@
* Creates a literal block for dumping
* @access private
* @return string
- * @param $value
+ * @param $value
* @param $indent int The value of the indent
- */
+ */
function _doLiteralBlock($value,$indent) {
$exploded = explode("\n",$value);
$newValue = '|';
@@ -443,7 +455,7 @@
}
return $newValue;
}
-
+
/**
* Folds a string of text, if necessary
* @access private
@@ -455,7 +467,7 @@
if ($this->_dumpWordWrap === 0) {
return $value;
}
-
+
if (strlen($value) > $this->_dumpWordWrap) {
$indent += $this->_dumpIndent;
$indent = str_repeat(' ',$indent);
@@ -464,9 +476,9 @@
}
return $value;
}
-
+
/* Methods used in loading */
-
+
/**
* Finds and returns the indentation of a YAML line
* @access private
@@ -491,7 +503,7 @@
* @param string $line A line from the YAML file
*/
function _parseLine($line) {
- $line = trim($line);
+ $line = trim($line);
$array = array();
@@ -534,7 +546,7 @@
}
return $array;
}
-
+
/**
* Finds the type of the passed value, returns the value as the new type.
* @access private
@@ -543,7 +555,7 @@
*/
function _toType($value) {
$matches = array();
- if (preg_match('/^("(.*)"|\'(.*)\')/',$value,$matches)) {
+ if (preg_match('/^("(.*)"|\'(.*)\')/',$value,$matches)) {
$value = (string)preg_replace('/(\'\'|\\\\\')/',"'",end($matches));
$value = preg_replace('/\\\\"/','"',$value);
} elseif (preg_match('/^\\[(.+)\\]$/',$value,$matches)) {
@@ -551,7 +563,7 @@
// Take out strings sequences and mappings
$explode = $this->_inlineEscape($matches[1]);
-
+
// Propogate value array
$value = array();
foreach ($explode as $v) {
@@ -581,10 +593,10 @@
$value = NULL;
} elseif (ctype_digit($value)) {
$value = (int)$value;
- } elseif (in_array(strtolower($value),
+ } elseif (in_array(strtolower($value),
array('true', 'on', '+', 'yes', 'y'))) {
$value = TRUE;
- } elseif (in_array(strtolower($value),
+ } elseif (in_array(strtolower($value),
array('false', 'off', '-', 'no', 'n'))) {
$value = FALSE;
} elseif (is_numeric($value)) {
@@ -593,10 +605,10 @@
// 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
@@ -607,13 +619,13 @@
// 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
+
+ // 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);
+ $inline = preg_replace($regex,'YAMLString',$inline);
}
unset($regex);
@@ -623,14 +635,14 @@
$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
@@ -654,7 +666,7 @@
}
}
}
-
+
// Re-add the mappings
if (!empty($maps)) {
$i = 0;
@@ -668,7 +680,7 @@
return $explode;
}
-
+
/**
* Builds the PHP array from all the YAML nodes we've gathered
* @access private
@@ -690,10 +702,10 @@
$trunk = $this->_array_kmerge($trunk,$n->data);
}
}
-
+
return $trunk;
}
-
+
/**
* Traverses node-space and sets references (& and *) accordingly
* @access private
@@ -705,7 +717,7 @@
if (!empty($node->data)) {
$key = key($node->data);
// If it's an array, don't check.
- if (is_array($node->data[$key])) {
+ if (is_array($node->data[$key])) {
foreach ($node->data[$key] as $k => $v) {
$this->_linkRef($node,$key,$k,$v);
}
@@ -713,11 +725,11 @@
$this->_linkRef($node,$key);
}
}
- }
+ }
}
return true;
}
-
+
function _linkRef(&$n,$key,$k = NULL,$v = NULL) {
if (empty($k) && empty($v)) {
// Look for &refs
@@ -725,7 +737,7 @@
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] =
+ $this->_allNodes[$n->id]->data[$key] =
substr($n->data[$key],strlen($matches[0])+1);
// Look for *refs
} elseif (preg_match('/^\*([^ ]+)/',$n->data[$key],$matches)) {
@@ -737,7 +749,7 @@
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] =
+ $this->_allNodes[$n->id]->data[$key][$k] =
substr($v,strlen($matches[0])+1);
// Look for *refs
} elseif (preg_match('/^\*([^ ]+)/',$v,$matches)) {
@@ -747,7 +759,7 @@
}
}
}
-
+
/**
* Finds the children of a node and aids in the building of the PHP array
* @access private
@@ -770,7 +782,7 @@
}
return $return;
}
-
+
/**
* Turns a node's data and its children's data into a PHP array
*
@@ -824,7 +836,7 @@
}
return true;
}
-
+
/**
* Merges arrays and maintains numeric keys.
@@ -832,29 +844,29 @@
* An ever-so-slightly modified version of the array_kmerge() function posted
* to php.net by mail at nospam dot iaindooley dot com on 2004-04-08.
*
- * http://us3.php.net/manual/en/function.array-merge.php#41394
+ * 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();
+ 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) {
+ $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.
@@ -862,11 +874,10 @@
while (array_key_exists($key, $ret)) {
$key++;
}
- }
- $ret[$key] = $val;
- }
+ }
+ $ret[$key] = $val;
+ }
- return $ret;
+ return $ret;
}
}
-
diff --git a/includes/api/ApiHelp.php b/includes/api/ApiHelp.php
index 47a45ea1..4ccb5acf 100644
--- a/includes/api/ApiHelp.php
+++ b/includes/api/ApiHelp.php
@@ -30,8 +30,8 @@ if (!defined('MEDIAWIKI')) {
/**
* This is a simple class to handle action=help
- *
- * @addtogroup API
+ *
+ * @ingroup API
*/
class ApiHelp extends ApiBase {
@@ -57,7 +57,6 @@ class ApiHelp extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiHelp.php 30222 2008-01-28 19:05:26Z catrope $';
+ return __CLASS__ . ': $Id: ApiHelp.php 35098 2008-05-20 17:13:28Z ialex $';
}
}
-
diff --git a/includes/api/ApiLogin.php b/includes/api/ApiLogin.php
index 3e66ed79..a45390c4 100644
--- a/includes/api/ApiLogin.php
+++ b/includes/api/ApiLogin.php
@@ -32,10 +32,10 @@ if (!defined('MEDIAWIKI')) {
/**
* Unit to authenticate log-in attempts to the current wiki.
*
- * @addtogroup API
+ * @ingroup API
*/
class ApiLogin extends ApiBase {
-
+
/**
* Time (in seconds) a user must wait after submitting
* a bad login (will be multiplied by the THROTTLE_FACTOR for each bad attempt)
@@ -47,12 +47,12 @@ class ApiLogin extends ApiBase {
* attempts is increased every failed attempt.
*/
const THROTTLE_FACTOR = 2;
-
+
/**
- * The maximum number of failed logins after which the wait increase stops.
+ * 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');
}
@@ -104,10 +104,15 @@ class ApiLogin extends ApiBase {
$wgUser->setOption('rememberpassword', 1);
$wgUser->setCookies();
+ // Run hooks. FIXME: split back and frontend from this hook.
+ // FIXME: This hook should be placed in the backend
+ $injected_html = '';
+ wfRunHooks('UserLoginComplete', array(&$wgUser, &$injected_html));
+
$result['result'] = 'Success';
- $result['lguserid'] = $_SESSION['wsUserID'];
- $result['lgusername'] = $_SESSION['wsUserName'];
- $result['lgtoken'] = $_SESSION['wsToken'];
+ $result['lguserid'] = $wgUser->getId();
+ $result['lgusername'] = $wgUser->getName();
+ $result['lgtoken'] = $wgUser->getToken();
$result['cookieprefix'] = $wgCookiePrefix;
$result['sessionid'] = session_id();
break;
@@ -130,34 +135,39 @@ class ApiLogin extends ApiBase {
case LoginForm :: EMPTY_PASS :
$result['result'] = 'EmptyPass';
break;
+ case LoginForm :: CREATE_BLOCKED :
+ $result['result'] = 'CreateBlocked';
+ $result['details'] = 'Your IP address is blocked from account creation';
+ break;
default :
ApiBase :: dieDebug(__METHOD__, 'Unhandled case value');
}
- if ($result['result'] != 'Success') {
- $result['wait'] = $this->cacheBadLogin();
- $result['details'] = "Please wait " . self::THROTTLE_TIME . " seconds before next log-in attempt";
+ if ($result['result'] != 'Success' && !isset( $result['details'] ) ) {
+ $delay = $this->cacheBadLogin();
+ $result['wait'] = $delay;
+ $result['details'] = "Please wait " . $delay . " seconds before next log-in attempt";
}
// 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
+ * 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.
+ * 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 );
@@ -167,24 +177,24 @@ class ApiLogin extends ApiBase {
} 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) );
-
+ $wgMemc->add( $key, $val, ApiLogin::calculateDelay(ApiLogin::THOTTLE_MAX_COUNT) );
+
return $delay;
}
-
+
/**
- * How much time the client must wait before it will be
+ * 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
@@ -192,7 +202,7 @@ class ApiLogin extends ApiBase {
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.
@@ -204,10 +214,10 @@ class ApiLogin extends ApiBase {
$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
+ * Internal cache key for badlogin checks. Robbed from the
* ConfirmEdit extension and modified to use a key unique to the
* API login.3
*
@@ -217,7 +227,7 @@ class ApiLogin extends ApiBase {
private function getMemCacheKey() {
return wfMemcKey( 'apilogin', 'badlogin', 'ip', wfGetIP() );
}
-
+
public function mustBePosted() { return true; }
public function getAllowedParams() {
@@ -241,11 +251,11 @@ class ApiLogin extends ApiBase {
'This module is used to login and get the authentication tokens. ',
'In the event of a successful log-in, a cookie will be attached',
'to your session. In the event of a failed log-in, you will not ',
- 'be able to attempt another log-in through this method for 60 seconds.',
+ 'be able to attempt another log-in through this method for 5 seconds.',
'This is to prevent password guessing by automated password crackers.'
);
}
-
+
protected function getExamples() {
return array(
'api.php?action=login&lgname=user&lgpassword=password'
@@ -253,7 +263,6 @@ class ApiLogin extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiLogin.php 30222 2008-01-28 19:05:26Z catrope $';
+ return __CLASS__ . ': $Id: ApiLogin.php 35565 2008-05-29 19:23:37Z btongminh $';
}
}
-
diff --git a/includes/api/ApiLogout.php b/includes/api/ApiLogout.php
index d578acf3..694c9e3c 100644
--- a/includes/api/ApiLogout.php
+++ b/includes/api/ApiLogout.php
@@ -29,10 +29,10 @@ if (!defined('MEDIAWIKI')) {
}
/**
- * API module to allow users to log out of the wiki. API equivalent of
+ * API module to allow users to log out of the wiki. API equivalent of
* Special:Userlogout.
*
- * @addtogroup API
+ * @ingroup API
*/
class ApiLogout extends ApiBase {
@@ -43,6 +43,10 @@ class ApiLogout extends ApiBase {
public function execute() {
global $wgUser;
$wgUser->logout();
+
+ // Give extensions to do something after user logout
+ $injected_html = '';
+ wfRunHooks( 'UserLogoutComplete', array(&$wgUser, &$injected_html) );
}
public function getAllowedParams() {
@@ -66,6 +70,6 @@ class ApiLogout extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id$';
+ return __CLASS__ . ': $Id: ApiLogout.php 35294 2008-05-24 20:44:49Z btongminh $';
}
}
diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php
index 874e531c..cce4c3e7 100644
--- a/includes/api/ApiMain.php
+++ b/includes/api/ApiMain.php
@@ -29,6 +29,10 @@ if (!defined('MEDIAWIKI')) {
}
/**
+ * @defgroup API API
+ */
+
+/**
* This is the main API class, used for both external and internal processing.
* When executed, it will create the requested formatter object,
* instantiate and execute an object associated with the needed action,
@@ -37,9 +41,9 @@ if (!defined('MEDIAWIKI')) {
*
* To use API from another application, run it using FauxRequest object, in which
* case any internal exceptions will not be handled but passed up to the caller.
- * After successful execution, use getResult() for the resulting data.
- *
- * @addtogroup API
+ * After successful execution, use getResult() for the resulting data.
+ *
+ * @ingroup API
*/
class ApiMain extends ApiBase {
@@ -62,7 +66,7 @@ class ApiMain extends ApiBase {
'help' => 'ApiHelp',
'paraminfo' => 'ApiParamInfo',
);
-
+
private static $WriteModules = array (
'rollback' => 'ApiRollback',
'delete' => 'ApiDelete',
@@ -71,8 +75,8 @@ class ApiMain extends ApiBase {
'block' => 'ApiBlock',
'unblock' => 'ApiUnblock',
'move' => 'ApiMove',
- #'changerights' => 'ApiChangeRights'
- # Disabled for now
+ 'edit' => 'ApiEditPage',
+ 'emailuser' => 'ApiEmailUser',
);
/**
@@ -113,25 +117,25 @@ class ApiMain extends ApiBase {
parent :: __construct($this, $this->mInternalMode ? 'main_int' : 'main');
if (!$this->mInternalMode) {
-
+
// Impose module restrictions.
- // If the current user cannot read,
+ // If the current user cannot read,
// Remove all modules other than login
global $wgUser;
-
+
if( $request->getVal( 'callback' ) !== null ) {
// JSON callback allows cross-site reads.
// For safety, strip user credentials.
wfDebug( "API: stripping user credentials for JSON callback\n" );
$wgUser = new User();
}
-
+
if (!$wgUser->isAllowed('read')) {
self::$Modules = array(
'login' => self::$Modules['login'],
'logout' => self::$Modules['logout'],
'help' => self::$Modules['help'],
- );
+ );
}
}
@@ -150,7 +154,8 @@ class ApiMain extends ApiBase {
$this->mRequest = & $request;
- $this->mSquidMaxage = 0;
+ $this->mSquidMaxage = -1; // flag for executeActionWithErrorHandling()
+ $this->mCommit = false;
}
/**
@@ -175,12 +180,19 @@ class ApiMain extends ApiBase {
}
/**
- * This method will simply cause an error if the write mode was disabled for this api.
+ * This method will simply cause an error if the write mode was disabled
+ * or if the current user doesn't have the right to use it
*/
public function requestWriteMode() {
+ global $wgUser;
if (!$this->mEnableWrite)
- $this->dieUsage('Editing of this site is disabled. Make sure the $wgEnableWriteAPI=true; ' .
- 'statement is included in the site\'s LocalSettings.php file', 'noapiwrite');
+ $this->dieUsage('Editing of this wiki through the API' .
+ ' is disabled. Make sure the $wgEnableWriteAPI=true; ' .
+ 'statement is included in the wiki\'s ' .
+ 'LocalSettings.php file', 'noapiwrite');
+ if (!$wgUser->isAllowed('writeapi'))
+ $this->dieUsage('You\'re not allowed to edit this ' .
+ 'wiki through the API', 'writeapidenied');
}
/**
@@ -198,7 +210,7 @@ class ApiMain extends ApiBase {
}
/**
- * Execute api request. Any errors will be handled if the API was called by the remote client.
+ * Execute api request. Any errors will be handled if the API was called by the remote client.
*/
public function execute() {
$this->profileIn();
@@ -206,6 +218,7 @@ class ApiMain extends ApiBase {
$this->executeAction();
else
$this->executeActionWithErrorHandling();
+
$this->profileOut();
}
@@ -247,11 +260,22 @@ 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);
+ }
+ else
+ $smaxage = $maxage = $this->mSquidMaxage;
+
// Set the cache expiration at the last moment, as any errors may change the expiration.
// if $this->mSquidMaxage == 0, the expiry time is set to the first second of unix epoch
- $expires = $this->mSquidMaxage == 0 ? 1 : time() + $this->mSquidMaxage;
+ $exp = min($smaxage, $maxage);
+ $expires = ($exp == 0 ? 1 : time() + $exp);
header('Expires: ' . wfTimestamp(TS_RFC2822, $expires));
- header('Cache-Control: s-maxage=' . $this->mSquidMaxage . ', must-revalidate, max-age=0');
+ header('Cache-Control: s-maxage=' . $smaxage . ', must-revalidate, max-age=' . $maxage);
if($this->mPrinter->getIsHtml())
echo wfReportTime();
@@ -261,10 +285,10 @@ class ApiMain extends ApiBase {
/**
* Replace the result data with the information about an exception.
- * Returns the error code
+ * Returns the error code
*/
protected function substituteResultWithError($e) {
-
+
// Printer may not be initialized if the extractRequestParams() fails for the main module
if (!isset ($this->mPrinter)) {
// The printer has not been created yet. Try to manually get formatter value.
@@ -284,20 +308,27 @@ class ApiMain extends ApiBase {
$errMessage = array (
'code' => $e->getCodeString(),
'info' => $e->getMessage());
-
+
// Only print the help message when this is for the developer, not runtime
if ($this->mPrinter->getIsHtml() || $this->mAction == 'help')
ApiResult :: setContent($errMessage, $this->makeHelpMsg());
} else {
+ global $wgShowSQLErrors, $wgShowExceptionDetails;
//
// Something is seriously wrong
//
+ if ( ( $e instanceof DBQueryError ) && !$wgShowSQLErrors ) {
+ $info = "Database query error";
+ } else {
+ $info = "Exception Caught: {$e->getMessage()}";
+ }
+
$errMessage = array (
'code' => 'internal_api_error_'. get_class($e),
- 'info' => "Exception Caught: {$e->getMessage()}"
+ 'info' => $info,
);
- ApiResult :: setContent($errMessage, "\n\n{$e->getTraceAsString()}\n\n");
+ ApiResult :: setContent($errMessage, $wgShowExceptionDetails ? "\n\n{$e->getTraceAsString()}\n\n" : "" );
}
$this->getResult()->reset();
@@ -318,12 +349,12 @@ class ApiMain extends ApiBase {
// Instantiate the module requested by the user
$module = new $this->mModules[$this->mAction] ($this, $this->mAction);
-
+
if( $module->shouldCheckMaxlag() && isset( $params['maxlag'] ) ) {
// Check for maxlag
- global $wgLoadBalancer, $wgShowHostnames;
+ global $wgShowHostnames;
$maxLag = $params['maxlag'];
- list( $host, $lag ) = $wgLoadBalancer->getMaxLag();
+ list( $host, $lag ) = wfGetLB()->getMaxLag();
if ( $lag > $maxLag ) {
if( $wgShowHostnames ) {
ApiBase :: dieUsage( "Waiting for $host: $lag seconds lagged", 'maxlag' );
@@ -333,7 +364,7 @@ class ApiMain extends ApiBase {
return;
}
}
-
+
if (!$this->mInternalMode) {
// Ignore mustBePosted() for internal calls
if($module->mustBePosted() && !$this->mRequest->wasPosted())
@@ -367,13 +398,12 @@ class ApiMain extends ApiBase {
protected function printResult($isError) {
$printer = $this->mPrinter;
$printer->profileIn();
-
+
/* If the help message is requested in the default (xmlfm) format,
* tell the printer not to escape ampersands so that our links do
* not break. */
- $params = $this->extractRequestParams();
- $printer->setUnescapeAmps ( ( $this->mAction == 'help' || $isError )
- && $params['format'] == ApiMain::API_DEFAULT_FORMAT );
+ $printer->setUnescapeAmps ( ( $this->mAction == 'help' || $isError )
+ && $this->getParameter('format') == ApiMain::API_DEFAULT_FORMAT );
$printer->initPrinter($isError);
@@ -399,6 +429,14 @@ class ApiMain extends ApiBase {
'maxlag' => array (
ApiBase :: PARAM_TYPE => 'integer'
),
+ 'smaxage' => array (
+ ApiBase :: PARAM_TYPE => 'integer',
+ ApiBase :: PARAM_DFLT => 0
+ ),
+ 'maxage' => array (
+ ApiBase :: PARAM_TYPE => 'integer',
+ ApiBase :: PARAM_DFLT => 0
+ ),
);
}
@@ -410,7 +448,9 @@ class ApiMain extends ApiBase {
'format' => 'The format of the output',
'action' => 'What action you would like to perform',
'version' => 'When showing help, include version for each module',
- 'maxlag' => 'Maximum lag'
+ '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',
);
}
@@ -444,13 +484,17 @@ class ApiMain extends ApiBase {
'',
);
}
-
+
/**
* Returns an array of strings with credits for the API
*/
protected function getCredits() {
return array(
- 'This API is being implemented by Roan Kattouw <Firstname>.<Lastname>@home.nl',
+ 'API developers:',
+ ' Roan Kattouw <Firstname>.<Lastname>@home.nl (lead developer Sep 2007-present)',
+ ' Victor Vasiliev - vasilvv at gee mail dot 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',
'or file a bug report at http://bugzilla.wikimedia.org/'
);
@@ -460,7 +504,7 @@ class ApiMain extends ApiBase {
* Override the parent to generate help messages for all available modules.
*/
public function makeHelpMsg() {
-
+
$this->mPrinter->setHelp();
// Use parent to make default message for the main module
@@ -486,9 +530,9 @@ class ApiMain extends ApiBase {
$msg .= $msg2;
$msg .= "\n";
}
-
+
$msg .= "\n*** Credits: ***\n " . implode("\n ", $this->getCredits()) . "\n";
-
+
return $msg;
}
@@ -496,15 +540,15 @@ class ApiMain extends ApiBase {
public static function makeHelpMsgHeader($module, $paramName) {
$modulePrefix = $module->getModulePrefix();
if (!empty($modulePrefix))
- $modulePrefix = "($modulePrefix) ";
-
+ $modulePrefix = "($modulePrefix) ";
+
return "* $paramName={$module->getModuleName()} $modulePrefix*";
- }
+ }
private $mIsBot = null;
private $mIsSysop = null;
private $mCanApiHighLimits = null;
-
+
/**
* Returns true if the currently logged in user is a bot, false otherwise
* OBSOLETE, use canApiHighLimits() instead
@@ -516,7 +560,7 @@ class ApiMain extends ApiBase {
}
return $this->mIsBot;
}
-
+
/**
* Similar to isBot(), this method returns true if the logged in user is
* a sysop, and false if not.
@@ -530,7 +574,11 @@ class ApiMain extends ApiBase {
return $this->mIsSysop;
}
-
+
+ /**
+ * Check whether the current user is allowed to use high limits
+ * @return bool
+ */
public function canApiHighLimits() {
if (!isset($this->mCanApiHighLimits)) {
global $wgUser;
@@ -540,6 +588,10 @@ class ApiMain extends ApiBase {
return $this->mCanApiHighLimits;
}
+ /**
+ * Check whether the user wants us to show version information in the API help
+ * @return bool
+ */
public function getShowVersions() {
return $this->mShowVersions;
}
@@ -551,7 +603,7 @@ class ApiMain extends ApiBase {
public function getVersion() {
$vers = array ();
$vers[] = 'MediaWiki ' . SpecialVersion::getVersion();
- $vers[] = __CLASS__ . ': $Id: ApiMain.php 31484 2008-03-03 05:46:20Z brion $';
+ $vers[] = __CLASS__ . ': $Id: ApiMain.php 37349 2008-07-08 20:53:41Z catrope $';
$vers[] = ApiBase :: getBaseVersion();
$vers[] = ApiFormatBase :: getBaseVersion();
$vers[] = ApiQueryBase :: getBaseVersion();
@@ -561,7 +613,7 @@ class ApiMain extends ApiBase {
/**
* Add or overwrite a module in this ApiMain instance. Intended for use by extending
- * classes who wish to add their own modules to their lexicon or override the
+ * classes who wish to add their own modules to their lexicon or override the
* behavior of inherent ones.
*
* @access protected
@@ -583,7 +635,7 @@ class ApiMain extends ApiBase {
protected function addFormat( $fmtName, $fmtClass ) {
$this->mFormats[$fmtName] = $fmtClass;
}
-
+
/**
* Get the array mapping module names to class names
*/
@@ -595,8 +647,8 @@ class ApiMain extends ApiBase {
/**
* This exception will be thrown when dieUsage is called to stop module execution.
* The exception handling code will print a help screen explaining how this API may be used.
- *
- * @addtogroup API
+ *
+ * @ingroup API
*/
class UsageException extends Exception {
@@ -613,5 +665,3 @@ class UsageException extends Exception {
return "{$this->getCodeString()}: {$this->getMessage()}";
}
}
-
-
diff --git a/includes/api/ApiMove.php b/includes/api/ApiMove.php
index a8c39c9a..8687bdcd 100644
--- a/includes/api/ApiMove.php
+++ b/includes/api/ApiMove.php
@@ -29,21 +29,21 @@ if (!defined('MEDIAWIKI')) {
/**
- * @addtogroup API
+ * @ingroup API
*/
class ApiMove extends ApiBase {
public function __construct($main, $action) {
parent :: __construct($main, $action);
}
-
+
public function execute() {
global $wgUser;
$this->getMain()->requestWriteMode();
$params = $this->extractRequestParams();
if(is_null($params['reason']))
$params['reason'] = '';
-
+
$titleObj = NULL;
if(!isset($params['from']))
$this->dieUsageMsg(array('missingparam', 'from'));
@@ -69,7 +69,7 @@ class ApiMove extends ApiBase {
// 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
+ // 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),
@@ -79,16 +79,19 @@ class ApiMove extends ApiBase {
// We don't care about multiple errors, just report one of them
$this->dieUsageMsg(current($errors));
- $dbw = wfGetDB(DB_MASTER);
- $dbw->begin();
+ $hookErr = null;
+
$retval = $fromTitle->moveTo($toTitle, true, $params['reason'], !$params['noredirect']);
if($retval !== true)
- $this->dieUsageMsg(array($retval));
+ {
+ # 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'])
+ if(!$params['noredirect'] || !$wgUser->isAllowed('suppressredirect'))
$r['redirectcreated'] = '';
-
+
if($params['movetalk'] && $fromTalk->exists() && !$fromTitle->isTalkPage())
{
// We need to move the talk page as well
@@ -104,14 +107,25 @@ class ApiMove extends ApiBase {
{
$r['talkmove-error-code'] = ApiBase::$messageMap[$retval]['code'];
$r['talkmove-error-info'] = ApiBase::$messageMap[$retval]['info'];
- }
+ }
+ }
+
+ # Watch pages
+ if($params['watch'] || $wgUser->getOption('watchmoves'))
+ {
+ $wgUser->addWatch($fromTitle);
+ $wgUser->addWatch($toTitle);
+ }
+ else if($params['unwatch'])
+ {
+ $wgUser->removeWatch($fromTitle);
+ $wgUser->removeWatch($toTitle);
}
- $dbw->commit(); // Make sure all changes are really written to the DB
$this->getResult()->addValue(null, $this->getModuleName(), $r);
}
-
+
public function mustBePosted() { return true; }
-
+
public function getAllowedParams() {
return array (
'from' => null,
@@ -119,7 +133,9 @@ class ApiMove extends ApiBase {
'token' => null,
'reason' => null,
'movetalk' => false,
- 'noredirect' => false
+ 'noredirect' => false,
+ 'watch' => false,
+ 'unwatch' => false
);
}
@@ -130,7 +146,9 @@ class ApiMove extends ApiBase {
'token' => 'A move token previously retrieved through prop=info',
'reason' => 'Reason for the move (optional).',
'movetalk' => 'Move the talk page, if it exists.',
- 'noredirect' => 'Don\'t create a redirect'
+ 'noredirect' => 'Don\'t create a redirect',
+ 'watch' => 'Add the page and the redirect to your watchlist',
+ 'unwatch' => 'Remove the page and the redirect from your watchlist'
);
}
@@ -147,6 +165,6 @@ class ApiMove extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiMove.php 30222 2008-01-28 19:05:26Z catrope $';
+ return __CLASS__ . ': $Id: ApiMove.php 35619 2008-05-30 19:59:47Z btongminh $';
}
}
diff --git a/includes/api/ApiOpenSearch.php b/includes/api/ApiOpenSearch.php
index f4b600fe..2da92059 100644
--- a/includes/api/ApiOpenSearch.php
+++ b/includes/api/ApiOpenSearch.php
@@ -29,7 +29,7 @@ if (!defined('MEDIAWIKI')) {
}
/**
- * @addtogroup API
+ * @ingroup API
*/
class ApiOpenSearch extends ApiBase {
@@ -45,11 +45,12 @@ class ApiOpenSearch extends ApiBase {
$params = $this->extractRequestParams();
$search = $params['search'];
$limit = $params['limit'];
-
+ $namespaces = $params['namespace'];
+
// Open search results may be stored for a very long time
$this->getMain()->setCacheMaxAge(1200);
-
- $srchres = PrefixSearch::titleSearch( $search, $limit );
+
+ $srchres = PrefixSearch::titleSearch( $search, $limit, $namespaces );
// Set top level elements
$result = $this->getResult();
@@ -66,14 +67,20 @@ class ApiOpenSearch extends ApiBase {
ApiBase :: PARAM_MIN => 1,
ApiBase :: PARAM_MAX => 100,
ApiBase :: PARAM_MAX2 => 100
- )
+ ),
+ 'namespace' => array(
+ ApiBase :: PARAM_DFLT => NS_MAIN,
+ ApiBase :: PARAM_TYPE => 'namespace',
+ ApiBase :: PARAM_ISMULTI => true
+ ),
);
}
public function getParamDescription() {
return array (
'search' => 'Search string',
- 'limit' => 'Maximum amount of results to return'
+ 'limit' => 'Maximum amount of results to return',
+ 'namespace' => 'Namespaces to search',
);
}
@@ -88,7 +95,6 @@ class ApiOpenSearch extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiOpenSearch.php 30275 2008-01-30 01:07:49Z brion $';
+ return __CLASS__ . ': $Id: ApiOpenSearch.php 35098 2008-05-20 17:13:28Z ialex $';
}
}
-
diff --git a/includes/api/ApiPageSet.php b/includes/api/ApiPageSet.php
index 185c0c59..e09cb285 100644
--- a/includes/api/ApiPageSet.php
+++ b/includes/api/ApiPageSet.php
@@ -33,17 +33,18 @@ if (!defined('MEDIAWIKI')) {
* Initially, when the client passes in titles=, pageids=, or revisions= parameter,
* an instance of the ApiPageSet class will normalize titles,
* determine if the pages/revisions exist, and prefetch any additional data page data requested.
- *
+ *
* When generator is used, the result of the generator will become the input for the
* second instance of this class, and all subsequent actions will go use the second instance
- * for all their work.
- *
- * @addtogroup API
+ * for all their work.
+ *
+ * @ingroup API
*/
class ApiPageSet extends ApiQueryBase {
- private $mAllPages; // [ns][dbkey] => page_id or 0 when missing
- private $mTitles, $mGoodTitles, $mMissingTitles, $mMissingPageIDs, $mRedirectTitles;
+ private $mAllPages; // [ns][dbkey] => page_id or negative when missing
+ private $mTitles, $mGoodTitles, $mMissingTitles, $mInvalidTitles;
+ private $mMissingPageIDs, $mRedirectTitles;
private $mNormalizedTitles, $mInterwikiTitles;
private $mResolveRedirects, $mPendingRedirectIDs;
private $mGoodRevIDs, $mMissingRevIDs;
@@ -58,6 +59,7 @@ class ApiPageSet extends ApiQueryBase {
$this->mTitles = array();
$this->mGoodTitles = array ();
$this->mMissingTitles = array ();
+ $this->mInvalidTitles = array ();
$this->mMissingPageIDs = array ();
$this->mRedirectTitles = array ();
$this->mNormalizedTitles = array ();
@@ -69,7 +71,7 @@ class ApiPageSet extends ApiQueryBase {
$this->mResolveRedirects = $resolveRedirects;
if($resolveRedirects)
$this->mPendingRedirectIDs = array();
-
+
$this->mFakePageId = -1;
}
@@ -107,8 +109,9 @@ class ApiPageSet extends ApiQueryBase {
}
/**
- * Returns an array [ns][dbkey] => page_id for all requested titles
- * page_id is a unique negative number in case title was not found
+ * Returns an array [ns][dbkey] => page_id for all requested titles.
+ * page_id is a unique negative number in case title was not found.
+ * Invalid titles will also have negative page IDs and will be in namespace 0
*/
public function getAllTitlesByNamespace() {
return $this->mAllPages;
@@ -154,6 +157,15 @@ class ApiPageSet extends ApiQueryBase {
}
/**
+ * Titles that were deemed invalid by Title::newFromText()
+ * The array's index will be unique and negative for each item
+ * @return array of strings (not Title objects)
+ */
+ public function getInvalidTitles() {
+ return $this->mInvalidTitles;
+ }
+
+ /**
* Page IDs that were not found in the database
* @return array of page IDs
*/
@@ -170,18 +182,18 @@ class ApiPageSet extends ApiQueryBase {
}
/**
- * Get a list of title normalizations - maps the title given
+ * Get a list of title normalizations - maps the title given
* with its normalized version.
- * @return array raw_prefixed_title (string) => prefixed_title (string)
+ * @return array raw_prefixed_title (string) => prefixed_title (string)
*/
public function getNormalizedTitles() {
return $this->mNormalizedTitles;
}
/**
- * Get a list of interwiki titles - maps the title given
+ * Get a list of interwiki titles - maps the title given
* with to the interwiki prefix.
- * @return array raw_prefixed_title (string) => interwiki_prefix (string)
+ * @return array raw_prefixed_title (string) => interwiki_prefix (string)
*/
public function getInterwikiTitles() {
return $this->mInterwikiTitles;
@@ -293,11 +305,11 @@ class ApiPageSet extends ApiQueryBase {
* Extract all requested fields from the row received from the database
*/
public function processDbRow($row) {
-
+
// Store Title object in various data structures
$title = Title :: makeTitle($row->page_namespace, $row->page_title);
-
- $pageId = intval($row->page_id);
+
+ $pageId = intval($row->page_id);
$this->mAllPages[$row->page_namespace][$row->page_title] = $pageId;
$this->mTitles[] = $title;
@@ -310,26 +322,26 @@ class ApiPageSet extends ApiQueryBase {
foreach ($this->mRequestedPageFields as $fieldName => & $fieldValues)
$fieldValues[$pageId] = $row-> $fieldName;
}
-
+
public function finishPageSetGeneration() {
$this->profileIn();
$this->resolvePendingRedirects();
$this->profileOut();
}
-
+
/**
* This method populates internal variables with page information
* based on the given array of title strings.
- *
+ *
* Steps:
* #1 For each title, get data from `page` table
* #2 If page was not found in the DB, store it as missing
- *
+ *
* Additionally, when resolving redirects:
* #3 If no more redirects left, stop.
* #4 For each redirect, get its links from `pagelinks` table.
* #5 Substitute the original LinkBatch object with the new list
- * #6 Repeat from step #1
+ * #6 Repeat from step #1
*/
private function initFromTitles($titles) {
@@ -337,7 +349,7 @@ class ApiPageSet extends ApiQueryBase {
$linkBatch = $this->processTitlesArray($titles);
if($linkBatch->isEmpty())
return;
-
+
$db = $this->getDB();
$set = $linkBatch->constructSet('page', $db);
@@ -368,13 +380,14 @@ class ApiPageSet extends ApiQueryBase {
$this->profileDBIn();
$res = $db->select('page', $this->getPageTableFields(), $set, __METHOD__);
$this->profileDBOut();
-
- $this->initFromQueryResult($db, $res, array_flip($pageids), false); // process PageIDs
+
+ $remaining = array_flip($pageids);
+ $this->initFromQueryResult($db, $res, $remaining, false); // process PageIDs
// Resolve any found redirects
$this->resolvePendingRedirects();
}
-
+
/**
* Iterate through the result of the query on 'page' table,
* and for each row create and store title object and save any extra fields requested.
@@ -390,7 +403,7 @@ class ApiPageSet extends ApiQueryBase {
private function initFromQueryResult($db, $res, &$remaining = null, $processTitles = null) {
if (!is_null($remaining) && is_null($processTitles))
ApiBase :: dieDebug(__METHOD__, 'Missing $processTitles parameter when $remaining is provided');
-
+
while ($row = $db->fetchObject($res)) {
$pageId = intval($row->page_id);
@@ -402,12 +415,12 @@ class ApiPageSet extends ApiQueryBase {
else
unset ($remaining[$pageId]);
}
-
+
// Store any extra fields requested by modules
$this->processDbRow($row);
}
$db->freeResult($res);
-
+
if(isset($remaining)) {
// Any items left in the $remaining list are added as missing
if($processTitles) {
@@ -437,15 +450,15 @@ class ApiPageSet extends ApiQueryBase {
if(empty($revids))
return;
-
+
$db = $this->getDB();
$pageids = array();
$remaining = array_flip($revids);
-
+
$tables = array('revision');
$fields = array('rev_id','rev_page');
$where = array('rev_deleted' => 0, 'rev_id' => $revids);
-
+
// Get pageIDs data from the `page` table
$this->profileDBIn();
$res = $db->select( $tables, $fields, $where, __METHOD__ );
@@ -472,27 +485,27 @@ class ApiPageSet extends ApiQueryBase {
if($this->mResolveRedirects) {
$db = $this->getDB();
$pageFlds = $this->getPageTableFields();
-
+
// Repeat until all redirects have been resolved
// The infinite loop is prevented by keeping all known pages in $this->mAllPages
- while (!empty ($this->mPendingRedirectIDs)) {
-
+ while (!empty ($this->mPendingRedirectIDs)) {
+
// Resolve redirects by querying the pagelinks table, and repeat the process
// Create a new linkBatch object for the next pass
$linkBatch = $this->getRedirectTargets();
-
+
if ($linkBatch->isEmpty())
break;
-
+
$set = $linkBatch->constructSet('page', $db);
if(false === $set)
break;
-
+
// Get pageIDs data from the `page` table
$this->profileDBIn();
$res = $db->select('page', $pageFlds, $set, __METHOD__);
$this->profileDBOut();
-
+
// Hack: get the ns:titles stored in array(ns => array(titles)) format
$this->initFromQueryResult($db, $res, $linkBatch->data, true);
}
@@ -500,76 +513,55 @@ class ApiPageSet extends ApiQueryBase {
}
private function getRedirectTargets() {
-
- $linkBatch = new LinkBatch();
+ $lb = new LinkBatch();
$db = $this->getDB();
- // find redirect targets for all redirect pages
$this->profileDBIn();
- $res = $db->select('pagelinks', array (
- 'pl_from',
- 'pl_namespace',
- 'pl_title'
- ), array (
- 'pl_from' => array_keys($this->mPendingRedirectIDs
- )), __METHOD__);
+ $res = $db->select('redirect', array(
+ 'rd_from',
+ 'rd_namespace',
+ 'rd_title'
+ ), array('rd_from' => array_keys($this->mPendingRedirectIDs)),
+ __METHOD__
+ );
$this->profileDBOut();
- while ($row = $db->fetchObject($res)) {
-
- $plfrom = intval($row->pl_from);
-
- // Bug 7304 workaround
- // ( http://bugzilla.wikipedia.org/show_bug.cgi?id=7304 )
- // A redirect page may have more than one link.
- // This code will only use the first link returned.
- if (isset ($this->mPendingRedirectIDs[$plfrom])) { // remove line when bug 7304 is fixed
-
- $titleStrFrom = $this->mPendingRedirectIDs[$plfrom]->getPrefixedText();
- $titleStrTo = Title :: makeTitle($row->pl_namespace, $row->pl_title)->getPrefixedText();
- unset ($this->mPendingRedirectIDs[$plfrom]); // remove line when bug 7304 is fixed
-
- // Avoid an infinite loop by checking if we have already processed this target
- if (!isset ($this->mAllPages[$row->pl_namespace][$row->pl_title])) {
- $linkBatch->add($row->pl_namespace, $row->pl_title);
- }
- } else {
- // This redirect page has more than one link.
- // This is very slow, but safer until bug 7304 is resolved
- $title = Title :: newFromID($plfrom);
- $titleStrFrom = $title->getPrefixedText();
-
+ while($row = $db->fetchObject($res))
+ {
+ $rdfrom = intval($row->rd_from);
+ $from = $this->mPendingRedirectIDs[$rdfrom]->getPrefixedText();
+ $to = Title::makeTitle($row->rd_namespace, $row->rd_title)->getPrefixedText();
+ unset($this->mPendingRedirectIDs[$rdfrom]);
+ if(!isset($this->mAllPages[$row->rd_namespace][$row->rd_title]))
+ $lb->add($row->rd_namespace, $row->rd_title);
+ $this->mRedirectTitles[$from] = $to;
+ }
+ $db->freeResult($res);
+ if(!empty($this->mPendingRedirectIDs))
+ {
+ # We found pages that aren't in the redirect table
+ # Add them
+ foreach($this->mPendingRedirectIDs as $id => $title)
+ {
$article = new Article($title);
- $text = $article->getContent();
- $titleTo = Title :: newFromRedirect($text);
- $titleStrTo = $titleTo->getPrefixedText();
-
- if (is_null($titleStrTo))
- ApiBase :: dieDebug(__METHOD__, 'Bug7304 workaround: redir target from {$title->getPrefixedText()} not found');
-
- // Avoid an infinite loop by checking if we have already processed this target
- if (!isset ($this->mAllPages[$titleTo->getNamespace()][$titleTo->getDBkey()])) {
- $linkBatch->addObj($titleTo);
- }
+ $rt = $article->insertRedirect();
+ if(!$rt)
+ # What the hell. Let's just ignore this
+ continue;
+ $lb->addObj($rt);
+ $this->mRedirectTitles[$title->getPrefixedText()] = $rt->getPrefixedText();
+ unset($this->mPendingRedirectIDs[$id]);
}
-
- $this->mRedirectTitles[$titleStrFrom] = $titleStrTo;
}
- $db->freeResult($res);
-
- // All IDs must exist in the page table
- if (!empty($this->mPendingRedirectIDs[$plfrom]))
- ApiBase :: dieDebug(__METHOD__, 'Invalid redirect IDs were found');
-
- return $linkBatch;
+ return $lb;
}
/**
* Given an array of title strings, convert them into Title objects.
* Alternativelly, an array of Title objects may be given.
- * This method validates access rights for the title,
+ * This method validates access rights for the title,
* and appends normalization values to the output.
- *
+ *
* @return LinkBatch of title objects.
*/
private function processTitlesArray($titles) {
@@ -577,16 +569,21 @@ class ApiPageSet extends ApiQueryBase {
$linkBatch = new LinkBatch();
foreach ($titles as $title) {
-
+
$titleObj = is_string($title) ? Title :: newFromText($title) : $title;
if (!$titleObj)
- $this->dieUsage("bad title", 'invalidtitle');
-
+ {
+ # Handle invalid titles gracefully
+ $this->mAllpages[0][$title] = $this->mFakePageId;
+ $this->mInvalidTitles[$this->mFakePageId] = $title;
+ $this->mFakePageId--;
+ continue; // There's nothing else we can do
+ }
$iw = $titleObj->getInterwiki();
if (!empty($iw)) {
// This title is an interwiki link.
$this->mInterwikiTitles[$titleObj->getPrefixedText()] = $iw;
- } else {
+ } else {
// Validation
if ($titleObj->getNamespace() < 0)
@@ -594,7 +591,7 @@ class ApiPageSet extends ApiQueryBase {
$linkBatch->addObj($titleObj);
}
-
+
// Make sure we remember the original title that was given to us
// This way the caller can correlate new titles with the originally requested,
// i.e. namespace is localized or capitalization is different
@@ -605,7 +602,7 @@ class ApiPageSet extends ApiQueryBase {
return $linkBatch;
}
-
+
protected function getAllowedParams() {
return array (
'titles' => array (
@@ -631,7 +628,6 @@ class ApiPageSet extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiPageSet.php 24935 2007-08-20 08:13:16Z nickj $';
+ return __CLASS__ . ': $Id: ApiPageSet.php 35098 2008-05-20 17:13:28Z ialex $';
}
}
-
diff --git a/includes/api/ApiParamInfo.php b/includes/api/ApiParamInfo.php
index 7de22252..77ce514f 100644
--- a/includes/api/ApiParamInfo.php
+++ b/includes/api/ApiParamInfo.php
@@ -29,7 +29,7 @@ if (!defined('MEDIAWIKI')) {
}
/**
- * @addtogroup API
+ * @ingroup API
*/
class ApiParamInfo extends ApiBase {
@@ -55,7 +55,7 @@ class ApiParamInfo extends ApiBase {
$obj = new $modArr[$m]($this->getMain(), $m);
$a = $this->getClassInfo($obj);
$a['name'] = $m;
- $r['modules'][] = $a;
+ $r['modules'][] = $a;
}
$result->setIndexedTagName($r['modules'], 'module');
}
@@ -106,7 +106,7 @@ class ApiParamInfo extends ApiBase {
$retval['parameters'][] = $a;
continue;
}
-
+
if(isset($p[ApiBase::PARAM_DFLT]))
$a['default'] = $p[ApiBase::PARAM_DFLT];
if(isset($p[ApiBase::PARAM_ISMULTI]))
@@ -131,7 +131,7 @@ class ApiParamInfo extends ApiBase {
$result->setIndexedTagName($retval['parameters'], 'param');
return $retval;
}
-
+
public function getAllowedParams() {
return array (
'modules' => array(
@@ -161,7 +161,6 @@ class ApiParamInfo extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiParse.php 29810 2008-01-15 21:33:08Z catrope $';
+ return __CLASS__ . ': $Id: ApiParamInfo.php 35098 2008-05-20 17:13:28Z ialex $';
}
}
-
diff --git a/includes/api/ApiParse.php b/includes/api/ApiParse.php
index 21a21e8d..4dcc94b6 100644
--- a/includes/api/ApiParse.php
+++ b/includes/api/ApiParse.php
@@ -29,7 +29,7 @@ if (!defined('MEDIAWIKI')) {
}
/**
- * @addtogroup API
+ * @ingroup API
*/
class ApiParse extends ApiBase {
@@ -43,31 +43,56 @@ class ApiParse extends ApiBase {
$text = $params['text'];
$title = $params['title'];
$page = $params['page'];
+ $oldid = $params['oldid'];
if(!is_null($page) && (!is_null($text) || $title != "API"))
$this->dieUsage("The page parameter cannot be used together with the text and title parameters", 'params');
$prop = array_flip($params['prop']);
-
+ $revid = false;
+
global $wgParser, $wgUser;
- if(!is_null($page)) {
- $titleObj = Title::newFromText($page);
- if(!$titleObj)
- $this->dieUsageMsg(array('missingtitle', $page));
-
- // Try the parser cache first
- $articleObj = new Article($titleObj);
- $pcache =& ParserCache::singleton();
- $p_result = $pcache->get($articleObj, $wgUser);
- if(!$p_result) {
- $p_result = $wgParser->parse($articleObj->getContent(), $titleObj, new ParserOptions());
- global $wgUseParserCache;
- if($wgUseParserCache)
- $pcache->save($p_result, $articleObj, $wgUser);
+ $popts = new ParserOptions();
+ $popts->setTidy(true);
+ $popts->enableLimitReport();
+ if(!is_null($oldid) || !is_null($page))
+ {
+ if(!is_null($oldid))
+ {
+ # Don't use the parser cache
+ $rev = Revision::newFromID($oldid);
+ if(!$rev)
+ $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();
+ $titleObj = $rev->getTitle();
+ $p_result = $wgParser->parse($text, $titleObj, $popts);
}
- } else {
+ else
+ {
+ $titleObj = Title::newFromText($page);
+ 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();
+ $pcache = ParserCache::singleton();
+ $p_result = $pcache->get($articleObj, $wgUser);
+ if(!$p_result) {
+ $p_result = $wgParser->parse($articleObj->getContent(), $titleObj, $popts);
+ global $wgUseParserCache;
+ if($wgUseParserCache)
+ $pcache->save($p_result, $articleObj, $wgUser);
+ }
+ }
+ }
+ else
+ {
$titleObj = Title::newFromText($title);
if(!$titleObj)
$titleObj = Title::newFromText("API");
- $p_result = $wgParser->parse($text, $titleObj, new ParserOptions());
+ $p_result = $wgParser->parse($text, $titleObj, $popts);
}
// Return result
@@ -91,6 +116,8 @@ class ApiParse extends ApiBase {
$result_array['externallinks'] = array_keys($p_result->getExternalLinks());
if(isset($prop['sections']))
$result_array['sections'] = $p_result->getSections();
+ if(!is_null($oldid))
+ $result_array['revid'] = $oldid;
$result_mapping = array(
'langlinks' => 'll',
@@ -104,7 +131,7 @@ class ApiParse extends ApiBase {
$this->setIndexedTagNames( $result_array, $result_mapping );
$result->addValue( null, $this->getModuleName(), $result_array );
}
-
+
private function formatLangLinks( $links ) {
$result = array();
foreach( $links as $link ) {
@@ -116,7 +143,7 @@ class ApiParse extends ApiBase {
}
return $result;
}
-
+
private function formatCategoryLinks( $links ) {
$result = array();
foreach( $links as $link => $sortkey ) {
@@ -127,7 +154,7 @@ class ApiParse extends ApiBase {
}
return $result;
}
-
+
private function formatLinks( $links ) {
$result = array();
foreach( $links as $ns => $nslinks ) {
@@ -142,7 +169,7 @@ class ApiParse extends ApiBase {
}
return $result;
}
-
+
private function setIndexedTagNames( &$array, $mapping ) {
foreach( $mapping as $key => $name ) {
if( isset( $array[$key] ) )
@@ -152,13 +179,14 @@ class ApiParse extends ApiBase {
public function getAllowedParams() {
return array (
- 'title' => array(
+ 'title' => array(
ApiBase :: PARAM_DFLT => 'API',
),
'text' => null,
'page' => null,
+ 'oldid' => null,
'prop' => array(
- ApiBase :: PARAM_DFLT => 'text|langlinks|categories|links|templates|images|externallinks|sections',
+ ApiBase :: PARAM_DFLT => 'text|langlinks|categories|links|templates|images|externallinks|sections|revid',
ApiBase :: PARAM_ISMULTI => true,
ApiBase :: PARAM_TYPE => array(
'text',
@@ -168,7 +196,8 @@ class ApiParse extends ApiBase {
'templates',
'images',
'externallinks',
- 'sections'
+ 'sections',
+ 'revid'
)
)
);
@@ -179,6 +208,7 @@ class ApiParse extends ApiBase {
'text' => 'Wikitext to parse',
'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'
),
@@ -196,7 +226,6 @@ class ApiParse extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiParse.php 30262 2008-01-29 14:47:27Z catrope $';
+ return __CLASS__ . ': $Id: ApiParse.php 36983 2008-07-03 15:01:50Z catrope $';
}
}
-
diff --git a/includes/api/ApiProtect.php b/includes/api/ApiProtect.php
index 40a4b73d..30bcfdbc 100644
--- a/includes/api/ApiProtect.php
+++ b/includes/api/ApiProtect.php
@@ -28,7 +28,7 @@ if (!defined('MEDIAWIKI')) {
}
/**
- * @addtogroup API
+ * @ingroup API
*/
class ApiProtect extends ApiBase {
@@ -40,7 +40,7 @@ class ApiProtect extends ApiBase {
global $wgUser;
$this->getMain()->requestWriteMode();
$params = $this->extractRequestParams();
-
+
$titleObj = NULL;
if(!isset($params['title']))
$this->dieUsageMsg(array('missingparam', 'title'));
@@ -55,12 +55,12 @@ class ApiProtect extends ApiBase {
$titleObj = Title::newFromText($params['title']);
if(!$titleObj)
$this->dieUsageMsg(array('invalidtitle', $params['title']));
-
+
$errors = $titleObj->getUserPermissionsErrors('protect', $wgUser);
if(!empty($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
@@ -68,7 +68,7 @@ class ApiProtect extends ApiBase {
$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'));
@@ -85,8 +85,6 @@ class ApiProtect extends ApiBase {
$this->dieUsageMsg(array('missingtitles-createonly'));
}
- $dbw = wfGetDb(DB_MASTER);
- $dbw->begin();
if($titleObj->exists()) {
$articleObj = new Article($titleObj);
$ok = $articleObj->updateRestrictions($protections, $params['reason'], $params['cascade'], $expiry);
@@ -96,7 +94,6 @@ class ApiProtect extends ApiBase {
// 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());
- $dbw->commit();
$res = array('title' => $titleObj->getPrefixedText(), 'reason' => $params['reason']);
if($expiry == Block::infinity())
$res['expiry'] = 'infinity';
@@ -149,6 +146,6 @@ class ApiProtect extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiProtect.php 30222 2008-01-28 19:05:26Z catrope $';
+ return __CLASS__ . ': $Id: ApiProtect.php 35098 2008-05-20 17:13:28Z ialex $';
}
}
diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php
index 29abd859..f4a2402f 100644
--- a/includes/api/ApiQuery.php
+++ b/includes/api/ApiQuery.php
@@ -33,11 +33,11 @@ if (!defined('MEDIAWIKI')) {
* it will create a list of titles to work on (an instance of the ApiPageSet object)
* instantiate and execute various property/list/meta modules,
* and assemble all resulting data into a single ApiResult object.
- *
+ *
* In the generator mode, a generator will be first executed to populate a second ApiPageSet object,
* and that object will be used for all subsequent modules.
- *
- * @addtogroup API
+ *
+ * @ingroup API
*/
class ApiQuery extends ApiBase {
@@ -55,9 +55,11 @@ class ApiQuery extends ApiBase {
'templates' => 'ApiQueryLinks',
'categories' => 'ApiQueryCategories',
'extlinks' => 'ApiQueryExternalLinks',
+ 'categoryinfo' => 'ApiQueryCategoryInfo',
);
private $mQueryListModules = array (
+ 'allimages' => 'ApiQueryAllimages',
'allpages' => 'ApiQueryAllpages',
'alllinks' => 'ApiQueryAllLinks',
'allcategories' => 'ApiQueryAllCategories',
@@ -90,7 +92,7 @@ class ApiQuery extends ApiBase {
public function __construct($main, $action) {
parent :: __construct($main, $action);
- // Allow custom modules to be added in LocalSettings.php
+ // Allow custom modules to be added in LocalSettings.php
global $wgApiQueryPropModules, $wgApiQueryListModules, $wgApiQueryMetaModules;
self :: appendUserModules($this->mQueryPropModules, $wgApiQueryPropModules);
self :: appendUserModules($this->mQueryListModules, $wgApiQueryListModules);
@@ -122,7 +124,7 @@ class ApiQuery extends ApiBase {
public function getDB() {
if (!isset ($this->mSlaveDB)) {
$this->profileDBIn();
- $this->mSlaveDB = wfGetDB(DB_SLAVE);
+ $this->mSlaveDB = wfGetDB(DB_SLAVE,'api');
$this->profileDBOut();
}
return $this->mSlaveDB;
@@ -130,9 +132,9 @@ class ApiQuery extends ApiBase {
/**
* Get the query database connection with the given name.
- * If no such connection has been requested before, it will be created.
- * Subsequent calls with the same $name will return the same connection
- * as the first, regardless of $db or $groups new values.
+ * If no such connection has been requested before, it will be created.
+ * Subsequent calls with the same $name will return the same connection
+ * as the first, regardless of $db or $groups new values.
*/
public function getNamedDB($name, $db, $groups) {
if (!array_key_exists($name, $this->mNamedDB)) {
@@ -149,7 +151,7 @@ class ApiQuery extends ApiBase {
public function getPageSet() {
return $this->mPageSet;
}
-
+
/**
* Get the array mapping module names to class names
*/
@@ -161,17 +163,17 @@ class ApiQuery extends ApiBase {
* Query execution happens in the following steps:
* #1 Create a PageSet object with any pages requested by the user
* #2 If using generator, execute it to get a new PageSet object
- * #3 Instantiate all requested modules.
+ * #3 Instantiate all requested modules.
* This way the PageSet object will know what shared data is required,
- * and minimize DB calls.
+ * and minimize DB calls.
* #4 Output all normalization and redirect resolution information
* #5 Execute all requested modules
*/
public function execute() {
-
+
$this->params = $this->extractRequestParams();
$this->redirects = $this->params['redirects'];
-
+
//
// Create PageSet
//
@@ -186,7 +188,7 @@ class ApiQuery extends ApiBase {
$this->InstantiateModules($modules, 'meta', $this->mQueryMetaModules);
//
- // If given, execute generator to substitute user supplied data with generated data.
+ // If given, execute generator to substitute user supplied data with generated data.
//
if (isset ($this->params['generator'])) {
$this->executeGeneratorModule($this->params['generator'], $modules);
@@ -210,21 +212,21 @@ class ApiQuery extends ApiBase {
$module->profileOut();
}
}
-
+
/**
* Query modules may optimize data requests through the $this->getPageSet() object
* by adding extra fields from the page table.
- * This function will gather all the extra request fields from the modules.
+ * This function will gather all the extra request fields from the modules.
*/
private function addCustomFldsToPageSet($modules, $pageSet) {
- // Query all requested modules.
+ // Query all requested modules.
foreach ($modules as $module) {
$module->requestExtraData($pageSet);
}
}
/**
- * Create instances of all modules requested by the client
+ * Create instances of all modules requested by the client
*/
private function InstantiateModules(&$modules, $param, $moduleList) {
$list = $this->params[$param];
@@ -235,7 +237,7 @@ class ApiQuery extends ApiBase {
/**
* Appends an element for each page in the current pageSet with the most general
- * information (id, title), plus any title normalizations and missing title/pageids/revids.
+ * information (id, title), plus any title normalizations and missing or invalid title/pageids/revids.
*/
private function outputGeneralPageInfo() {
@@ -255,7 +257,7 @@ class ApiQuery extends ApiBase {
$result->setIndexedTagName($normValues, 'n');
$result->addValue('query', 'normalized', $normValues);
}
-
+
// Interwiki titles
$intrwValues = array ();
foreach ($pageSet->getInterwikiTitles() as $rawTitleStr => $interwikiStr) {
@@ -269,12 +271,12 @@ class ApiQuery extends ApiBase {
$result->setIndexedTagName($intrwValues, 'i');
$result->addValue('query', 'interwiki', $intrwValues);
}
-
+
// Show redirect information
$redirValues = array ();
foreach ($pageSet->getRedirectTitles() as $titleStrFrom => $titleStrTo) {
$redirValues[] = array (
- 'from' => $titleStrFrom,
+ 'from' => strval($titleStrFrom),
'to' => $titleStrTo
);
}
@@ -311,7 +313,9 @@ class ApiQuery extends ApiBase {
$vals['missing'] = '';
$pages[$fakeId] = $vals;
}
-
+ // Report any invalid titles
+ foreach ($pageSet->getInvalidTitles() as $fakeId => $title)
+ $pages[$fakeId] = array('title' => $title, 'invalid' => '');
// Report any missing page ids
foreach ($pageSet->getMissingPageIDs() as $pageid) {
$pages[$pageid] = array (
@@ -329,7 +333,7 @@ class ApiQuery extends ApiBase {
}
if (!empty ($pages)) {
-
+
if ($this->params['indexpageids']) {
$pageIDs = array_keys($pages);
// json treats all map keys as strings - converting to match
@@ -337,14 +341,14 @@ class ApiQuery extends ApiBase {
$result->setIndexedTagName($pageIDs, 'id');
$result->addValue('query', 'pageids', $pageIDs);
}
-
+
$result->setIndexedTagName($pages, 'page');
$result->addValue('query', 'pages', $pages);
}
}
/**
- * For generator mode, execute generator, and use its output as new pageSet
+ * For generator mode, execute generator, and use its output as new pageSet
*/
protected function executeGeneratorModule($generatorName, $modules) {
@@ -357,7 +361,7 @@ class ApiQuery extends ApiBase {
ApiBase :: dieDebug(__METHOD__, "Unknown generator=$generatorName");
}
- // Generator results
+ // Generator results
$resultPageSet = new ApiPageSet($this, $this->redirects);
// Create and execute the generator
@@ -386,7 +390,7 @@ class ApiQuery extends ApiBase {
/**
* Returns the list of allowed parameters for this module.
- * Qurey module also lists all ApiPageSet parameters as its own.
+ * Qurey module also lists all ApiPageSet parameters as its own.
*/
public function getAllowedParams() {
return array (
@@ -423,12 +427,14 @@ class ApiQuery extends ApiBase {
$this->mAllowedGenerators = array(); // Will be repopulated
$astriks = str_repeat('--- ', 8);
+ $astriks2 = str_repeat('*** ', 10);
$msg .= "\n$astriks Query: Prop $astriks\n\n";
$msg .= $this->makeHelpMsgHelper($this->mQueryPropModules, 'prop');
$msg .= "\n$astriks Query: List $astriks\n\n";
$msg .= $this->makeHelpMsgHelper($this->mQueryListModules, 'list');
$msg .= "\n$astriks Query: Meta $astriks\n\n";
$msg .= $this->makeHelpMsgHelper($this->mQueryMetaModules, 'meta');
+ $msg .= "\n\n$astriks2 Modules: continuation $astriks2\n\n";
// Perform the base call last because the $this->mAllowedGenerators
// will be updated inside makeHelpMsgHelper()
@@ -469,7 +475,7 @@ class ApiQuery extends ApiBase {
$psModule = new ApiPageSet($this);
return $psModule->makeHelpMsgParameters() . parent :: makeHelpMsgParameters();
}
-
+
// @todo should work correctly
public function shouldCheckMaxlag() {
return true;
@@ -503,9 +509,8 @@ class ApiQuery extends ApiBase {
public function getVersion() {
$psModule = new ApiPageSet($this);
$vers = array ();
- $vers[] = __CLASS__ . ': $Id: ApiQuery.php 30222 2008-01-28 19:05:26Z catrope $';
+ $vers[] = __CLASS__ . ': $Id: ApiQuery.php 35098 2008-05-20 17:13:28Z ialex $';
$vers[] = $psModule->getVersion();
return $vers;
}
}
-
diff --git a/includes/api/ApiQueryAllCategories.php b/includes/api/ApiQueryAllCategories.php
index 84494876..3ff42c88 100644
--- a/includes/api/ApiQueryAllCategories.php
+++ b/includes/api/ApiQueryAllCategories.php
@@ -31,8 +31,8 @@ if (!defined('MEDIAWIKI')) {
/**
* Query module to enumerate all categories, even the ones that don't have
* category pages.
- *
- * @addtogroup API
+ *
+ * @ingroup API
*/
class ApiQueryAllCategories extends ApiQueryGeneratorBase {
@@ -53,44 +53,58 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase {
$db = $this->getDB();
$params = $this->extractRequestParams();
- $this->addTables('categorylinks');
- $this->addFields('cl_to');
-
+ $this->addTables('category');
+ $this->addFields('cat_title');
+
if (!is_null($params['from']))
- $this->addWhere('cl_to>=' . $db->addQuotes(ApiQueryBase :: titleToKey($params['from'])));
+ $this->addWhere('cat_title>=' . $db->addQuotes($this->titleToKey($params['from'])));
if (isset ($params['prefix']))
- $this->addWhere("cl_to LIKE '" . $db->escapeLike(ApiQueryBase :: titleToKey($params['prefix'])) . "%'");
+ $this->addWhere("cat_title LIKE '" . $db->escapeLike($this->titleToKey($params['prefix'])) . "%'");
$this->addOption('LIMIT', $params['limit']+1);
- $this->addOption('ORDER BY', 'cl_to' . ($params['dir'] == 'descending' ? ' DESC' : ''));
- $this->addOption('DISTINCT');
+ $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']) );
$res = $this->select(__METHOD__);
$pages = array();
+ $categories = array();
+ $result = $this->getResult();
$count = 0;
while ($row = $db->fetchObject($res)) {
if (++ $count > $params['limit']) {
// We've reached the one extra which shows that there are additional cats to be had. Stop here...
// TODO: Security issue - if the user has no right to view next title, it will still be shown
- $this->setContinueEnumParameter('from', ApiQueryBase :: keyToTitle($row->cl_to));
+ $this->setContinueEnumParameter('from', $this->keyToTitle($row->cat_title));
break;
}
-
+
// Normalize titles
- $titleObj = Title::makeTitle(NS_CATEGORY, $row->cl_to);
+ $titleObj = Title::makeTitle(NS_CATEGORY, $row->cat_title);
if(!is_null($resultPageSet))
$pages[] = $titleObj->getPrefixedText();
- else
- // Don't show "Category:" everywhere in non-generator mode
- $pages[] = $titleObj->getText();
+ else {
+ $item = array();
+ $result->setContent( $item, $titleObj->getText() );
+ if( isset( $prop['size'] ) ) {
+ $item['size'] = $row->cat_pages;
+ $item['pages'] = $row->cat_pages - $row->cat_subcats - $row->cat_files;
+ $item['files'] = $row->cat_files;
+ $item['subcats'] = $row->cat_subcats;
+ }
+ if( isset( $prop['hidden'] ) && $row->cat_hidden )
+ $item['hidden'] = '';
+ $categories[] = $item;
+ }
}
$db->freeResult($res);
if (is_null($resultPageSet)) {
- $result = $this->getResult();
- $result->setIndexedTagName($pages, 'c');
- $result->addValue('query', $this->getModuleName(), $pages);
+ $result->setIndexedTagName($categories, 'c');
+ $result->addValue('query', $this->getModuleName(), $categories);
} else {
$resultPageSet->populateFromTitles($pages);
}
@@ -113,7 +127,12 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase {
ApiBase :: PARAM_MIN => 1,
ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1,
ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2
- )
+ ),
+ 'prop' => array (
+ ApiBase :: PARAM_TYPE => array( 'size', 'hidden' ),
+ ApiBase :: PARAM_DFLT => '',
+ ApiBase :: PARAM_ISMULTI => true
+ ),
);
}
@@ -122,7 +141,8 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase {
'from' => 'The category to start enumerating from.',
'prefix' => 'Search for all category titles that begin with this value.',
'dir' => 'Direction to sort in.',
- 'limit' => 'How many categories to return.'
+ 'limit' => 'How many categories to return.',
+ 'prop' => 'Which properties to get',
);
}
@@ -132,11 +152,12 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase {
protected function getExamples() {
return array (
+ 'api.php?action=query&list=allcategories&acprop=size',
'api.php?action=query&generator=allcategories&gacprefix=List&prop=info',
);
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryAllLinks.php 28216 2007-12-06 18:33:18Z vasilievvv $';
+ return __CLASS__ . ': $Id: ApiQueryAllCategories.php 36790 2008-06-29 22:26:23Z catrope $';
}
}
diff --git a/includes/api/ApiQueryAllLinks.php b/includes/api/ApiQueryAllLinks.php
index d5b80644..aefbb725 100644
--- a/includes/api/ApiQueryAllLinks.php
+++ b/includes/api/ApiQueryAllLinks.php
@@ -30,8 +30,8 @@ if (!defined('MEDIAWIKI')) {
/**
* Query module to enumerate links from all pages together.
- *
- * @addtogroup API
+ *
+ * @ingroup API
*/
class ApiQueryAllLinks extends ApiQueryGeneratorBase {
@@ -67,26 +67,37 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
$this->addTables('pagelinks');
$this->addWhereFld('pl_namespace', $params['namespace']);
+ if (!is_null($params['from']) && !is_null($params['continue']))
+ $this->dieUsage('alcontinue and alfrom cannot be used together', 'params');
+ if (!is_null($params['continue']))
+ {
+ $arr = explode('|', $params['continue']);
+ if(count($arr) != 2)
+ $this->dieUsage("Invalid continue parameter", 'badcontinue');
+ $params['from'] = $arr[0]; // Handled later
+ $id = intval($arr[1]);
+ $this->addWhere("pl_from >= $id");
+ }
+
if (!is_null($params['from']))
- $this->addWhere('pl_title>=' . $db->addQuotes(ApiQueryBase :: titleToKey($params['from'])));
+ $this->addWhere('pl_title>=' . $db->addQuotes($this->titleToKey($params['from'])));
if (isset ($params['prefix']))
- $this->addWhere("pl_title LIKE '" . $db->escapeLike(ApiQueryBase :: titleToKey($params['prefix'])) . "%'");
+ $this->addWhere("pl_title LIKE '" . $db->escapeLike($this->titleToKey($params['prefix'])) . "%'");
- if (is_null($resultPageSet)) {
- $this->addFields(array (
- 'pl_namespace',
- 'pl_title'
- ));
- $this->addFieldsIf('pl_from', $fld_ids);
- } else {
- $this->addFields('pl_from');
- $pageids = array();
- }
+ $this->addFields(array (
+ 'pl_namespace',
+ 'pl_title',
+ 'pl_from'
+ ));
$this->addOption('USE INDEX', 'pl_namespace');
$limit = $params['limit'];
$this->addOption('LIMIT', $limit+1);
- $this->addOption('ORDER BY', 'pl_namespace, pl_title');
+ # 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
+ $this->addOption('ORDER BY', 'pl_title');
$res = $this->select(__METHOD__);
@@ -96,7 +107,7 @@ 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('from', ApiQueryBase :: keyToTitle($row->pl_title));
+ $this->setContinueEnumParameter('continue', $this->keyToTitle($row->pl_title) . "|" . $row->pl_from);
break;
}
@@ -127,6 +138,7 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
public function getAllowedParams() {
return array (
+ 'continue' => null,
'from' => null,
'prefix' => null,
'unique' => false,
@@ -159,7 +171,8 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
'unique' => 'Only show unique links. Cannot be used with generator or prop=ids',
'prop' => 'What pieces of information to include',
'namespace' => 'The namespace to enumerate.',
- 'limit' => 'How many total links to return.'
+ 'limit' => 'How many total links to return.',
+ 'continue' => 'When more results are available, use this to continue.',
);
}
@@ -174,6 +187,6 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryAllLinks.php 30222 2008-01-28 19:05:26Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryAllLinks.php 37258 2008-07-07 14:48:40Z catrope $';
}
}
diff --git a/includes/api/ApiQueryAllUsers.php b/includes/api/ApiQueryAllUsers.php
index e055b3c5..dd0e98a8 100644
--- a/includes/api/ApiQueryAllUsers.php
+++ b/includes/api/ApiQueryAllUsers.php
@@ -30,8 +30,8 @@ if (!defined('MEDIAWIKI')) {
/**
* Query module to enumerate all registered users.
- *
- * @addtogroup API
+ *
+ * @ingroup API
*/
class ApiQueryAllUsers extends ApiQueryBase {
@@ -46,26 +46,27 @@ class ApiQueryAllUsers extends ApiQueryBase {
$prop = $params['prop'];
if (!is_null($prop)) {
$prop = array_flip($prop);
+ $fld_blockinfo = isset($prop['blockinfo']);
$fld_editcount = isset($prop['editcount']);
$fld_groups = isset($prop['groups']);
$fld_registration = isset($prop['registration']);
- } else {
- $fld_editcount = $fld_groups = $fld_registration = false;
+ } else {
+ $fld_blockinfo = $fld_editcount = $fld_groups = $fld_registration = false;
}
$limit = $params['limit'];
- $tables = $db->tableName('user');
-
+ $this->addTables('user', 'u1');
+
if( !is_null( $params['from'] ) )
- $this->addWhere( 'user_name >= ' . $db->addQuotes( self::keyToTitle( $params['from'] ) ) );
-
+ $this->addWhere( 'u1.user_name >= ' . $db->addQuotes( $this->keyToTitle( $params['from'] ) ) );
+
if( isset( $params['prefix'] ) )
- $this->addWhere( 'user_name LIKE "' . $db->escapeLike( self::keyToTitle( $params['prefix'] ) ) . '%"' );
+ $this->addWhere( 'u1.user_name LIKE "' . $db->escapeLike( $this->keyToTitle( $params['prefix'] ) ) . '%"' );
if (!is_null($params['group'])) {
// Filter only users that belong to a given group
- $tblName = $db->tableName('user_groups');
- $tables = "$tables INNER JOIN $tblName ug1 ON ug1.ug_user=user_id";
+ $this->addTables('user_groups', 'ug1');
+ $this->addWhere('ug1.ug_user=u1.user_id');
$this->addWhereFld('ug1.ug_group', $params['group']);
}
@@ -75,23 +76,30 @@ class ApiQueryAllUsers extends ApiQueryBase {
$groupCount = count(User::getAllGroups());
$sqlLimit = $limit+$groupCount+1;
- $tblName = $db->tableName('user_groups');
- $tables = "$tables LEFT JOIN $tblName ug2 ON ug2.ug_user=user_id";
+ $this->addTables('user_groups', 'ug2');
+ $tname = $this->getAliasedName('user_groups', 'ug2');
+ $this->addJoinConds(array($tname => array('LEFT JOIN', 'ug2.ug_user=u1.user_id')));
$this->addFields('ug2.ug_group ug_group2');
} else {
$sqlLimit = $limit+1;
}
-
- if ($fld_registration)
- $this->addFields('user_registration');
+ if ($fld_blockinfo) {
+ $this->addTables('ipblocks');
+ $this->addTables('user', 'u2');
+ $u2 = $this->getAliasedName('user', 'u2');
+ $this->addJoinConds(array(
+ 'ipblocks' => array('LEFT JOIN', 'ipb_user=u1.user_id'),
+ $u2 => array('LEFT JOIN', 'ipb_by=u2.user_id')));
+ $this->addFields(array('ipb_reason', 'u2.user_name blocker_name'));
+ }
$this->addOption('LIMIT', $sqlLimit);
- $this->addTables($tables);
- $this->addFields('user_name');
- $this->addFieldsIf('user_editcount', $fld_editcount);
+ $this->addFields('u1.user_name');
+ $this->addFieldsIf('u1.user_editcount', $fld_editcount);
+ $this->addFieldsIf('u1.user_registration', $fld_registration);
- $this->addOption('ORDER BY', 'user_name');
+ $this->addOption('ORDER BY', 'u1.user_name');
$res = $this->select(__METHOD__);
@@ -100,7 +108,7 @@ class ApiQueryAllUsers extends ApiQueryBase {
$lastUserData = false;
$lastUser = false;
$result = $this->getResult();
-
+
//
// This loop keeps track of the last entry.
// For each new row, if the new row is for different user then the last, the last entry is added to results.
@@ -109,49 +117,53 @@ class ApiQueryAllUsers extends ApiQueryBase {
// to make sure all rows that belong to the same user are received.
//
while (true) {
-
+
$row = $db->fetchObject($res);
$count++;
-
+
if (!$row || $lastUser != $row->user_name) {
// Save the last pass's user data
if (is_array($lastUserData))
$data[] = $lastUserData;
-
+
// No more rows left
if (!$row)
break;
if ($count > $limit) {
// We've reached the one extra which shows that there are additional pages to be had. Stop here...
- $this->setContinueEnumParameter('from', ApiQueryBase :: keyToTitle($row->user_name));
+ $this->setContinueEnumParameter('from', $this->keyToTitle($row->user_name));
break;
}
// Record new user's data
$lastUser = $row->user_name;
$lastUserData = array( 'name' => $lastUser );
+ if ($fld_blockinfo) {
+ $lastUserData['blockedby'] = $row->blocker_name;
+ $lastUserData['blockreason'] = $row->ipb_reason;
+ }
if ($fld_editcount)
$lastUserData['editcount'] = intval($row->user_editcount);
if ($fld_registration)
$lastUserData['registration'] = wfTimestamp(TS_ISO_8601, $row->user_registration);
-
+
}
-
+
if ($sqlLimit == $count) {
// BUG! database contains group name that User::getAllGroups() does not return
// TODO: should handle this more gracefully
- ApiBase :: dieDebug(__METHOD__,
+ ApiBase :: dieDebug(__METHOD__,
'MediaWiki configuration error: the database contains more user groups than known to User::getAllGroups() function');
}
-
+
// Add user's group info
if ($fld_groups && !is_null($row->ug_group2)) {
$lastUserData['groups'][] = $row->ug_group2;
$result->setIndexedTagName($lastUserData['groups'], 'g');
}
}
-
+
$db->freeResult($res);
$result->setIndexedTagName($data, 'u');
@@ -166,11 +178,12 @@ class ApiQueryAllUsers extends ApiQueryBase {
ApiBase :: PARAM_TYPE => User::getAllGroups()
),
'prop' => array (
- ApiBase :: PARAM_ISMULTI => true,
+ ApiBase :: PARAM_ISMULTI => true,
ApiBase :: PARAM_TYPE => array (
- 'editcount',
+ 'blockinfo',
'groups',
- 'registration',
+ 'editcount',
+ 'registration'
)
),
'limit' => array (
@@ -206,6 +219,6 @@ class ApiQueryAllUsers extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryAllUsers.php 30222 2008-01-28 19:05:26Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryAllUsers.php 36790 2008-06-29 22:26:23Z catrope $';
}
}
diff --git a/includes/api/ApiQueryAllimages.php b/includes/api/ApiQueryAllimages.php
new file mode 100644
index 00000000..26cbc368
--- /dev/null
+++ b/includes/api/ApiQueryAllimages.php
@@ -0,0 +1,205 @@
+<?php
+
+/*
+ * Created on Mar 16, 2008
+ *
+ * API for MediaWiki 1.12+
+ *
+ * Copyright (C) 2008 Vasiliev Victor vasilvv@gmail.com,
+ * based on ApiQueryAllpages.php
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ('ApiQueryBase.php');
+}
+
+/**
+ * Query module to enumerate all available pages.
+ *
+ * @ingroup API
+ */
+class ApiQueryAllimages extends ApiQueryGeneratorBase {
+
+ public function __construct($query, $moduleName) {
+ parent :: __construct($query, $moduleName, 'ai');
+ }
+
+ public function execute() {
+ $this->run();
+ }
+
+ public function executeGenerator($resultPageSet) {
+ if ($resultPageSet->isResolvingRedirects())
+ $this->dieUsage('Use "gaifilterredir=nonredirects" option instead of "redirects" when using allimages as a generator', 'params');
+
+ $this->run($resultPageSet);
+ }
+
+ private function run($resultPageSet = null) {
+ $repo = RepoGroup::singleton()->getLocalRepo();
+ if ( !$repo instanceof LocalRepo )
+ $this->dieUsage('Local file repository does not support querying all images', 'unsupportedrepo');
+
+ $db = $this->getDB();
+
+ $params = $this->extractRequestParams();
+
+ // Image filters
+ if (!is_null($params['from']))
+ $this->addWhere('img_name>=' . $db->addQuotes($this->titleToKey($params['from'])));
+ if (isset ($params['prefix']))
+ $this->addWhere("img_name LIKE '" . $db->escapeLike($this->titleToKey($params['prefix'])) . "%'");
+
+ if (isset ($params['minsize'])) {
+ $this->addWhere('img_size>=' . intval($params['minsize']));
+ }
+
+ if (isset ($params['maxsize'])) {
+ $this->addWhere('img_size<=' . intval($params['maxsize']));
+ }
+
+ $sha1 = false;
+ if( isset( $params['sha1'] ) ) {
+ $sha1 = wfBaseConvert( $params['sha1'], 16, 36, 31 );
+ } elseif( isset( $params['sha1base36'] ) ) {
+ $sha1 = $params['sha1base36'];
+ }
+ if( $sha1 ) {
+ $this->addWhere( 'img_sha1=' . $db->addQuotes( $sha1 ) );
+ }
+
+ $this->addTables('image');
+
+ $prop = array_flip($params['prop']);
+ $this->addFields( LocalFile::selectFields() );
+
+ $limit = $params['limit'];
+ $this->addOption('LIMIT', $limit+1);
+ $this->addOption('ORDER BY', 'img_name' .
+ ($params['dir'] == 'descending' ? ' DESC' : ''));
+
+ $res = $this->select(__METHOD__);
+
+ $data = array ();
+ $count = 0;
+ $result = $this->getResult();
+ while ($row = $db->fetchObject($res)) {
+ if (++ $count > $limit) {
+ // We've reached the one extra which shows that there are additional pages to be had. Stop here...
+ // TODO: Security issue - if the user has no right to view next title, it will still be shown
+ $this->setContinueEnumParameter('from', $this->keyToTitle($row->img_name));
+ break;
+ }
+
+ if (is_null($resultPageSet)) {
+ $file = $repo->newFileFromRow( $row );
+
+ $data[] = ApiQueryImageInfo::getInfo( $file, $prop, $result );
+ } else {
+ $data[] = Title::makeTitle( NS_IMAGE, $row->img_name );
+ }
+ }
+ $db->freeResult($res);
+
+ if (is_null($resultPageSet)) {
+ $result = $this->getResult();
+ $result->setIndexedTagName($data, 'img');
+ $result->addValue('query', $this->getModuleName(), $data);
+ } else {
+ $resultPageSet->populateFromTitles( $data );
+ }
+ }
+
+ public function getAllowedParams() {
+ return array (
+ 'from' => null,
+ 'prefix' => null,
+ 'minsize' => array (
+ ApiBase :: PARAM_TYPE => 'integer',
+ ),
+ 'maxsize' => array (
+ ApiBase :: PARAM_TYPE => 'integer',
+ ),
+ '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
+ ),
+ 'dir' => array (
+ ApiBase :: PARAM_DFLT => 'ascending',
+ ApiBase :: PARAM_TYPE => array (
+ 'ascending',
+ 'descending'
+ )
+ ),
+ 'sha1' => null,
+ 'sha1base36' => null,
+ 'prop' => array (
+ ApiBase :: PARAM_TYPE => array(
+ 'timestamp',
+ 'user',
+ 'comment',
+ 'url',
+ 'size',
+ 'dimensions', // Obsolete
+ 'mime',
+ 'sha1',
+ 'metadata'
+ ),
+ ApiBase :: PARAM_DFLT => 'timestamp|url',
+ ApiBase :: PARAM_ISMULTI => true
+ )
+ );
+ }
+
+ public function getParamDescription() {
+ return array (
+ 'from' => 'The image title to start enumerating from.',
+ 'prefix' => 'Search for all image titles that begin with this value.',
+ 'dir' => 'The direction in which to list',
+ 'minsize' => 'Limit to images with at least this many bytes',
+ 'maxsize' => 'Limit to images with at most this many bytes',
+ 'limit' => 'How many total images to return.',
+ 'sha1' => 'SHA1 hash of image',
+ 'sha1base36' => 'SHA1 hash of image in base 36 (used in MediaWiki)',
+ 'prop' => 'Which properties to get',
+ );
+ }
+
+ public function getDescription() {
+ return 'Enumerate all images sequentially';
+ }
+
+ protected function getExamples() {
+ return array (
+ 'Simple Use',
+ ' Show a list of images starting at the letter "B"',
+ ' api.php?action=query&list=allimages&aifrom=B',
+ 'Using as Generator',
+ ' Show info about 4 images starting at the letter "T"',
+ ' api.php?action=query&generator=allimages&gailimit=4&gaifrom=T&prop=imageinfo',
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiQueryAllimages.php 37909 2008-07-22 13:26:15Z catrope $';
+ }
+}
diff --git a/includes/api/ApiQueryAllmessages.php b/includes/api/ApiQueryAllmessages.php
index b7c86a91..06683379 100644
--- a/includes/api/ApiQueryAllmessages.php
+++ b/includes/api/ApiQueryAllmessages.php
@@ -30,8 +30,8 @@ if (!defined('MEDIAWIKI')) {
/**
* A query action to return messages from site message cache
- *
- * @addtogroup API
+ *
+ * @ingroup API
*/
class ApiQueryAllmessages extends ApiQueryBase {
@@ -42,13 +42,13 @@ class ApiQueryAllmessages extends ApiQueryBase {
public function execute() {
global $wgMessageCache;
$params = $this->extractRequestParams();
-
+
if(!is_null($params['lang']))
{
global $wgLang;
$wgLang = Language::factory($params['lang']);
}
-
+
//Determine which messages should we print
$messages_target = array();
@@ -60,7 +60,7 @@ class ApiQueryAllmessages extends ApiQueryBase {
} else {
$messages_target = explode( '|', $params['messages'] );
}
-
+
//Filter messages
if( isset( $params['filter'] ) ) {
$messages_filtered = array();
@@ -72,12 +72,9 @@ class ApiQueryAllmessages extends ApiQueryBase {
$messages_target = $messages_filtered;
}
- $wgMessageCache->disableTransform();
-
//Get all requested messages
$messages = array();
foreach( $messages_target as $message ) {
- $message = trim( $message ); //Message list can be formatted like "msg1 | msg2 | msg3", so let's trim() it
$messages[$message] = wfMsg( $message );
}
@@ -87,7 +84,11 @@ class ApiQueryAllmessages extends ApiQueryBase {
foreach( $messages as $name => $value ) {
$message = array();
$message['name'] = $name;
- $result->setContent( $message, $value );
+ if( wfEmptyMsg( $name, $value ) ) {
+ $message['missing'] = '';
+ } else {
+ $result->setContent( $message, $value );
+ }
$messages_out[] = $message;
}
$result->setIndexedTagName( $messages_out, 'message' );
@@ -107,8 +108,8 @@ class ApiQueryAllmessages extends ApiQueryBase {
public function getParamDescription() {
return array (
'messages' => 'Which messages to output. "*" means all messages',
- 'filter' => 'Return only messages that contains specified string',
- 'lang' => 'Language code',
+ 'filter' => 'Return only messages that contain this string',
+ 'lang' => 'Return messages in this language',
);
}
@@ -124,6 +125,6 @@ class ApiQueryAllmessages extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryAllmessages.php 30222 2008-01-28 19:05:26Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryAllmessages.php 37504 2008-07-10 14:28:09Z catrope $';
}
}
diff --git a/includes/api/ApiQueryAllpages.php b/includes/api/ApiQueryAllpages.php
index 280d1de2..39490fe7 100644
--- a/includes/api/ApiQueryAllpages.php
+++ b/includes/api/ApiQueryAllpages.php
@@ -30,8 +30,8 @@ if (!defined('MEDIAWIKI')) {
/**
* Query module to enumerate all available pages.
- *
- * @addtogroup API
+ *
+ * @ingroup API
*/
class ApiQueryAllpages extends ApiQueryGeneratorBase {
@@ -55,27 +55,29 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
$db = $this->getDB();
$params = $this->extractRequestParams();
-
+
// Page filters
+ $this->addTables('page');
if (!$this->addWhereIf('page_is_redirect = 1', $params['filterredir'] === 'redirects'))
$this->addWhereIf('page_is_redirect = 0', $params['filterredir'] === 'nonredirects');
$this->addWhereFld('page_namespace', $params['namespace']);
- if (!is_null($params['from']))
- $this->addWhere('page_title>=' . $db->addQuotes(ApiQueryBase :: titleToKey($params['from'])));
+ $dir = ($params['dir'] == 'descending' ? 'older' : 'newer');
+ $from = (is_null($params['from']) ? null : $this->titleToKey($params['from']));
+ $this->addWhereRange('page_title', $dir, $from, null);
if (isset ($params['prefix']))
- $this->addWhere("page_title LIKE '" . $db->escapeLike(ApiQueryBase :: titleToKey($params['prefix'])) . "%'");
+ $this->addWhere("page_title LIKE '" . $db->escapeLike($this->titleToKey($params['prefix'])) . "%'");
$forceNameTitleIndex = true;
if (isset ($params['minsize'])) {
$this->addWhere('page_len>=' . intval($params['minsize']));
$forceNameTitleIndex = false;
}
-
+
if (isset ($params['maxsize'])) {
$this->addWhere('page_len<=' . intval($params['maxsize']));
$forceNameTitleIndex = false;
}
-
+
// Page protection filtering
if (isset ($params['prtype'])) {
$this->addTables('page_restrictions');
@@ -86,7 +88,7 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
$prlevel = $params['prlevel'];
if (!is_null($prlevel) && $prlevel != '' && $prlevel != '*')
$this->addWhereFld('pr_level', $prlevel);
-
+
$this->addOption('DISTINCT');
$forceNameTitleIndex = false;
@@ -94,20 +96,16 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
} else if (isset ($params['prlevel'])) {
$this->dieUsage('prlevel may not be used without prtype', 'params');
}
-
+
if($params['filterlanglinks'] == 'withoutlanglinks') {
- $pageName = $this->getDB()->tableName('page');
- $llName = $this->getDB()->tableName('langlinks');
- $tables = "$pageName LEFT JOIN $llName ON page_id=ll_from";
+ $this->addTables('langlinks');
+ $this->addJoinConds(array('langlinks' => array('LEFT JOIN', 'page_id=ll_from')));
$this->addWhere('ll_from IS NULL');
- $this->addTables($tables);
$forceNameTitleIndex = false;
} else if($params['filterlanglinks'] == 'withlanglinks') {
- $this->addTables(array('page', 'langlinks'));
+ $this->addTables('langlinks');
$this->addWhere('page_id=ll_from');
- $forceNameTitleIndex = false;
- } else {
- $this->addTables('page');
+ $forceNameTitleIndex = false;
}
if ($forceNameTitleIndex)
$this->addOption('USE INDEX', 'name_title');
@@ -124,9 +122,6 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
$limit = $params['limit'];
$this->addOption('LIMIT', $limit+1);
- $this->addOption('ORDER BY', 'page_namespace, page_title' .
- ($params['dir'] == 'descending' ? ' DESC' : ''));
-
$res = $this->select(__METHOD__);
$data = array ();
@@ -135,7 +130,7 @@ class ApiQueryAllpages 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('from', ApiQueryBase :: keyToTitle($row->page_title));
+ $this->setContinueEnumParameter('from', $this->keyToTitle($row->page_title));
break;
}
@@ -160,7 +155,7 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
public function getAllowedParams() {
global $wgRestrictionTypes, $wgRestrictionLevels;
-
+
return array (
'from' => null,
'prefix' => null,
@@ -178,10 +173,10 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
),
'minsize' => array (
ApiBase :: PARAM_TYPE => 'integer',
- ),
+ ),
'maxsize' => array (
ApiBase :: PARAM_TYPE => 'integer',
- ),
+ ),
'prtype' => array (
ApiBase :: PARAM_TYPE => $wgRestrictionTypes,
ApiBase :: PARAM_ISMULTI => true
@@ -249,7 +244,6 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryAllpages.php 30222 2008-01-28 19:05:26Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryAllpages.php 37775 2008-07-17 09:26:01Z brion $';
}
}
-
diff --git a/includes/api/ApiQueryBacklinks.php b/includes/api/ApiQueryBacklinks.php
index 1ca5c33a..fea058f3 100644
--- a/includes/api/ApiQueryBacklinks.php
+++ b/includes/api/ApiQueryBacklinks.php
@@ -29,18 +29,18 @@ if (!defined('MEDIAWIKI')) {
}
/**
- * This is three-in-one module to query:
+ * This is a three-in-one module to query:
* * backlinks - links pointing to the given page,
* * embeddedin - what pages transclude the given page within themselves,
* * imageusage - what pages use the given image
- *
- * @addtogroup API
+ *
+ * @ingroup API
*/
class ApiQueryBacklinks extends ApiQueryGeneratorBase {
- private $params, $rootTitle, $contRedirs, $contLevel, $contTitle, $contID;
+ private $params, $rootTitle, $contRedirs, $contLevel, $contTitle, $contID, $redirID;
- // output element name, database column field prefix, database table
+ // output element name, database column field prefix, database table
private $backlinksSettings = array (
'backlinks' => array (
'code' => 'bl',
@@ -66,10 +66,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
parent :: __construct($query, $moduleName, $code);
$this->bl_ns = $prefix . '_namespace';
$this->bl_from = $prefix . '_from';
- $this->bl_tables = array (
- $linktbl,
- 'page'
- );
+ $this->bl_table = $linktbl;
$this->bl_code = $code;
$this->hasNS = $moduleName !== 'imageusage';
@@ -97,207 +94,219 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
$this->run($resultPageSet);
}
- private function run($resultPageSet = null) {
- $this->params = $this->extractRequestParams();
-
- $redirect = $this->params['redirect'];
- if ($redirect)
- $this->dieDebug('Redirect has not been implemented', 'notimplemented');
-
- $this->processContinue();
-
- $this->addFields($this->bl_fields);
- if (is_null($resultPageSet))
- $this->addFields(array (
- 'page_id',
- 'page_namespace',
- 'page_title'
- ));
+ private function prepareFirstQuery($resultPageSet = null) {
+ /* SELECT page_id, page_title, page_namespace, page_is_redirect
+ * FROM pagelinks, page WHERE pl_from=page_id
+ * AND pl_title='Foo' AND pl_namespace=0
+ * LIMIT 11 ORDER BY pl_from
+ */
+ $db = $this->getDb();
+ $this->addTables(array('page', $this->bl_table));
+ $this->addWhere("{$this->bl_from}=page_id");
+ if(is_null($resultPageSet))
+ $this->addFields(array('page_id', 'page_title', 'page_namespace'));
else
- $this->addFields($resultPageSet->getPageTableFields()); // will include page_id
-
- $this->addTables($this->bl_tables);
- $this->addWhere($this->bl_from . '=page_id');
-
- if ($this->hasNS)
+ $this->addFields($resultPageSet->getPageTableFields());
+ $this->addFields('page_is_redirect');
+ $this->addWhereFld($this->bl_title, $this->rootTitle->getDbKey());
+ if($this->hasNS)
$this->addWhereFld($this->bl_ns, $this->rootTitle->getNamespace());
- $this->addWhereFld($this->bl_title, $this->rootTitle->getDBkey());
$this->addWhereFld('page_namespace', $this->params['namespace']);
-
+ if(!is_null($this->contID))
+ $this->addWhere("page_id>={$this->contID}");
if($this->params['filterredir'] == 'redirects')
$this->addWhereFld('page_is_redirect', 1);
if($this->params['filterredir'] == 'nonredirects')
$this->addWhereFld('page_is_redirect', 0);
+ $this->addOption('LIMIT', $this->params['limit'] + 1);
+ $this->addOption('ORDER BY', $this->bl_from);
+ }
- $limit = $this->params['limit'];
- $this->addOption('LIMIT', $limit +1);
+ 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
+ */
+ $db = $this->getDb();
+ $this->addTables(array('page', $this->bl_table));
+ $this->addWhere("{$this->bl_from}=page_id");
+ if(is_null($resultPageSet))
+ $this->addFields(array('page_id', 'page_title', 'page_namespace', 'page_is_redirect'));
+ else
+ $this->addFields($resultPageSet->getPageTableFields());
+ $this->addFields($this->bl_title);
+ if($this->hasNS)
+ $this->addFields($this->bl_ns);
+ $titleWhere = '';
+ foreach($this->redirTitles as $t)
+ $titleWhere .= ($titleWhere != '' ? " OR " : '') .
+ "({$this->bl_title} = ".$db->addQuotes($t->getDBKey()).
+ ($this->hasNS ? " AND {$this->bl_ns} = '{$t->getNamespace()}'" : "") .
+ ")";
+ $this->addWhere($titleWhere);
+ $this->addWhereFld('page_namespace', $this->params['namespace']);
+ if(!is_null($this->redirID))
+ $this->addWhere("page_id>={$this->redirID}");
+ if($this->params['filterredir'] == 'redirects')
+ $this->addWhereFld('page_is_redirect', 1);
+ if($this->params['filterredir'] == 'nonredirects')
+ $this->addWhereFld('page_is_redirect', 0);
+ $this->addOption('LIMIT', $this->params['limit'] + 1);
$this->addOption('ORDER BY', $this->bl_sort);
+ }
- $db = $this->getDB();
- if (!is_null($this->params['continue'])) {
- $plfrm = intval($this->contID);
- if ($this->contLevel == 0) {
- // For the first level, there is only one target title, so no need for complex filtering
- $this->addWhere($this->bl_from . '>=' . $plfrm);
- } else {
- $ns = $this->contTitle->getNamespace();
- $t = $db->addQuotes($this->contTitle->getDBkey());
- $whereWithoutNS = "{$this->bl_title}>$t OR ({$this->bl_title}=$t AND {$this->bl_from}>=$plfrm))";
-
- if ($this->hasNS)
- $this->addWhere("{$this->bl_ns}>$ns OR ({$this->bl_ns}=$ns AND ($whereWithoutNS)");
- else
- $this->addWhere($whereWithoutNS);
- }
+ private function run($resultPageSet = null) {
+ $this->params = $this->extractRequestParams(false);
+ $this->redirect = isset($this->params['redirect']) && $this->params['redirect'];
+ $userMax = ( $this->redirect ? ApiBase::LIMIT_BIG1/2 : ApiBase::LIMIT_BIG1 );
+ $botMax = ( $this->redirect ? ApiBase::LIMIT_BIG2/2 : ApiBase::LIMIT_BIG2 );
+ if( $this->params['limit'] == 'max' ) {
+ $this->params['limit'] = $this->getMain()->canApiHighLimits() ? $botMax : $userMax;
+ $this->getResult()->addValue( 'limits', $this->getModuleName(), $this->params['limit'] );
}
+ $this->processContinue();
+ $this->prepareFirstQuery($resultPageSet);
+
+ $db = $this->getDB();
$res = $this->select(__METHOD__);
$count = 0;
- $data = array ();
+ $this->data = array ();
+ $this->continueStr = null;
+ $this->redirTitles = array();
while ($row = $db->fetchObject($res)) {
- if (++ $count > $limit) {
+ if (++ $count > $this->params['limit']) {
// We've reached the one extra which shows that there are additional pages to be had. Stop here...
- if ($redirect) {
- $ns = $row-> { $this->bl_ns };
- $t = $row-> { $this->bl_title };
- $continue = $this->getContinueRedirStr(false, 0, $ns, $t, $row->page_id);
- } else
- $continue = $this->getContinueStr($row->page_id);
- // TODO: Security issue - if the user has no right to view next title, it will still be shown
- $this->setContinueEnumParameter('continue', $continue);
+ // Continue string preserved in case the redirect query doesn't pass the limit
+ $this->continueStr = $this->getContinueStr($row->page_id);
break;
}
- if (is_null($resultPageSet)) {
- $vals = $this->extractRowInfo($row);
- if ($vals)
- $data[] = $vals;
- } else {
+ if (is_null($resultPageSet))
+ $this->extractRowInfo($row);
+ else
+ {
+ if($row->page_is_redirect)
+ $this->redirTitles[] = Title::makeTitle($row->page_namespace, $row->page_title);
$resultPageSet->processDbRow($row);
}
}
$db->freeResult($res);
+ if($this->redirect && !empty($this->redirTitles))
+ {
+ $this->resetQueryParams();
+ $this->prepareSecondQuery($resultPageSet);
+ $res = $this->select(__METHOD__);
+ $count = 0;
+ while($row = $db->fetchObject($res))
+ {
+ if(++$count > $this->params['limit'])
+ {
+ // We've reached the one extra which shows that there are additional pages to be had. Stop here...
+ // We need to keep the parent page of this redir in
+ if($this->hasNS)
+ $contTitle = Title::makeTitle($row->{$this->bl_ns}, $row->{$this->bl_title});
+ else
+ $contTitle = Title::makeTitle(NS_IMAGE, $row->{$this->bl_title});
+ $this->continueStr = $this->getContinueRedirStr($contTitle->getArticleID(), $row->page_id);
+ break;
+ }
+
+ if(is_null($resultPageSet))
+ $this->extractRedirRowInfo($row);
+ else
+ $resultPageSet->processDbRow($row);
+ }
+ $db->freeResult($res);
+ }
+ if(!is_null($this->continueStr))
+ $this->setContinueEnumParameter('continue', $this->continueStr);
+
if (is_null($resultPageSet)) {
+ $resultData = array();
+ foreach($this->data as $ns => $a)
+ foreach($a as $title => $arr)
+ $resultData[$arr['pageid']] = $arr;
$result = $this->getResult();
- $result->setIndexedTagName($data, $this->bl_code);
- $result->addValue('query', $this->getModuleName(), $data);
+ $result->setIndexedTagName($resultData, $this->bl_code);
+ $result->addValue('query', $this->getModuleName(), $resultData);
}
}
private function extractRowInfo($row) {
+ if(!isset($this->data[$row->page_namespace][$row->page_title])) {
+ $this->data[$row->page_namespace][$row->page_title]['pageid'] = $row->page_id;
+ ApiQueryBase::addTitleInfo($this->data[$row->page_namespace][$row->page_title], Title::makeTitle($row->page_namespace, $row->page_title));
+ if($row->page_is_redirect)
+ {
+ $this->data[$row->page_namespace][$row->page_title]['redirect'] = '';
+ $this->redirTitles[] = Title::makeTitle($row->page_namespace, $row->page_title);
+ }
+ }
+ }
- $vals = array();
- $vals['pageid'] = intval($row->page_id);
- ApiQueryBase :: addTitleInfo($vals, Title :: makeTitle($row->page_namespace, $row->page_title));
-
- return $vals;
+ private function extractRedirRowInfo($row)
+ {
+ $a['pageid'] = $row->page_id;
+ 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;
+ $this->data[$ns][$row->{$this->bl_title}]['redirlinks'][] = $a;
+ $this->getResult()->setIndexedTagName($this->data[$ns][$row->{$this->bl_title}]['redirlinks'], $this->bl_code);
}
protected function processContinue() {
- $pageSet = $this->getPageSet();
- $count = $pageSet->getTitleCount();
-
- if (!is_null($this->params['continue'])) {
+ if (!is_null($this->params['continue']))
$this->parseContinueParam();
-
- // Skip all completed links
-
- } else {
- $title = $this->params['title'];
- if (!is_null($title)) {
- $this->rootTitle = Title :: newFromText($title);
- } else { // This case is obsolete. Will support this for a while
- if ($count !== 1)
- $this->dieUsage("The {$this->getModuleName()} query requires one title to start", 'bad_title_count');
- $this->rootTitle = current($pageSet->getTitles()); // only one title there
- $this->setWarning('Using titles parameter is obsolete for this list. Use ' . $this->encodeParamName('title') . ' instead.');
+ else {
+ if ( $this->params['title'] !== "" ) {
+ $title = Title::newFromText( $this->params['title'] );
+ if ( !$title ) {
+ $this->dieUsageMsg(array('invalidtitle', $this->params['title']));
+ } else {
+ $this->rootTitle = $title;
+ }
+ } else {
+ $this->dieUsageMsg(array('missingparam', 'title'));
}
}
- // only image titles are allowed for the root
+ // only image titles are allowed for the root in imageinfo mode
if (!$this->hasNS && $this->rootTitle->getNamespace() !== NS_IMAGE)
$this->dieUsage("The title for {$this->getModuleName()} query must be an image", 'bad_image_title');
}
protected function parseContinueParam() {
$continueList = explode('|', $this->params['continue']);
- if ($this->params['redirect']) {
- //
- // expected redirect-mode parameter:
- // ns|db_key|step|level|ns|db_key|id
- // ns+db_key -- the root title
- // step = 1 or 2 - which step to continue from - 1-titles, 2-redirects
- // level -- how many levels to follow before starting enumerating.
- // if level > 0 -- ns+title to continue from, otherwise skip these
- // id = last page_id to continue from
- //
- if (count($continueList) > 4) {
- $rootNs = intval($continueList[0]);
- if (($rootNs !== 0 || $continueList[0] === '0') && !empty ($continueList[1])) {
- $this->rootTitle = Title :: makeTitleSafe($rootNs, $continueList[1]);
- if ($this->rootTitle) {
-
- $step = intval($continueList[2]);
- if ($step === 1 || $step === 2) {
- $this->contRedirs = ($step === 2);
-
- $level = intval($continueList[3]);
- if ($level !== 0 || $continueList[3] === '0') {
- $this->contLevel = $level;
-
- if ($level === 0) {
- if (count($continueList) === 5) {
- $contID = intval($continueList[4]);
- if ($contID !== 0 || $continueList[4] === '0') {
- $this->contID = $contID;
- return; // done
- }
- }
- } else {
- if (count($continueList) === 7) {
- $contNs = intval($continueList[4]);
- if (($contNs !== 0 || $continueList[4] === '0') && !empty ($continueList[5])) {
- $this->contTitle = Title :: makeTitleSafe($contNs, $continueList[5]);
-
- $contID = intval($continueList[6]);
- if ($contID !== 0 || $continueList[6] === '0') {
- $this->contID = $contID;
- return; // done
- }
- }
- }
- }
- }
- }
- }
- }
- }
- } else {
- //
- // expected non-redirect-mode parameter:
- // ns|db_key|id
- // ns+db_key -- the root title
- // id = last page_id to continue from
- //
- if (count($continueList) === 3) {
- $rootNs = intval($continueList[0]);
- if (($rootNs !== 0 || $continueList[0] === '0') && !empty ($continueList[1])) {
- $this->rootTitle = Title :: makeTitleSafe($rootNs, $continueList[1]);
- if ($this->rootTitle) {
-
- $contID = intval($continueList[2]);
- if ($contID !== 0) {
- $this->contID = $contID;
- return; // done
- }
- }
- }
- }
- }
+ // expected format:
+ // ns | key | id1 [| id2]
+ // ns+key: root title
+ // id1: first-level page ID to continue from
+ // id2: second-level page ID to continue from
+
+ // null stuff out now so we know what's set and what isn't
+ $this->rootTitle = $this->contID = $this->redirID = null;
+ $rootNs = intval($continueList[0]);
+ if($rootNs === 0 && $continueList[0] !== '0')
+ // Illegal continue parameter
+ $this->dieUsage("Invalid continue param. You should pass the original value returned by the previous query", "_badcontinue");
+ $this->rootTitle = Title::makeTitleSafe($rootNs, $continueList[1]);
+ if(!$this->rootTitle)
+ $this->dieUsage("Invalid continue param. You should pass the original value returned by the previous query", "_badcontinue");
+ $contID = intval($continueList[2]);
+ if($contID === 0 && $continueList[2] !== '0')
+ $this->dieUsage("Invalid continue param. You should pass the original value returned by the previous query", "_badcontinue");
+ $this->contID = $contID;
+ $redirID = intval(@$continueList[3]);
+ if($redirID === 0 && @$continueList[3] !== '0')
+ // This one isn't required
+ return;
+ $this->redirID = $redirID;
- $this->dieUsage("Invalid continue param. You should pass the original value returned by the previous query", "_badcontinue");
}
protected function getContinueStr($lastPageID) {
@@ -306,18 +315,12 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
'|' . $lastPageID;
}
- protected function getContinueRedirStr($isRedirPhase, $level, $ns, $title, $lastPageID) {
- return $this->rootTitle->getNamespace() .
- '|' . $this->rootTitle->getDBkey() .
- '|' . ($isRedirPhase ? 1 : 2) .
- '|' . $level .
- ($level > 0 ? ('|' . $ns . '|' . $title) : '') .
- '|' . $lastPageID;
+ protected function getContinueRedirStr($lastPageID, $lastRedirID) {
+ return $this->getContinueStr($lastPageID) . '|' . $lastRedirID;
}
public function getAllowedParams() {
-
- return array (
+ $retval = array (
'title' => null,
'continue' => null,
'namespace' => array (
@@ -332,7 +335,6 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
'nonredirects'
)
),
- 'redirect' => false,
'limit' => array (
ApiBase :: PARAM_DFLT => 10,
ApiBase :: PARAM_TYPE => 'limit',
@@ -341,17 +343,27 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2
)
);
+ if($this->getModuleName() == 'embeddedin')
+ return $retval;
+ $retval['redirect'] = false;
+ return $retval;
}
public function getParamDescription() {
- return array (
+ $retval = array (
'title' => 'Title to search. If null, titles= parameter will be used instead, but will be obsolete soon.',
'continue' => 'When more results are available, use this to continue.',
'namespace' => 'The namespace to enumerate.',
- 'filterredir' => 'How to filter for redirects',
- 'redirect' => 'If linking page is a redirect, find all pages that link to that redirect (not implemented)',
- 'limit' => 'How many total pages to return.'
+ 'filterredir' => 'How to filter for redirects'
);
+ if($this->getModuleName() != 'embeddedin')
+ return array_merge($retval, array(
+ 'redirect' => 'If linking page is a redirect, find all pages that link to that redirect as well. Maximum limit is halved.',
+ 'limit' => "How many total pages to return. If {$this->bl_code}redirect is enabled, limit applies to each level separately."
+ ));
+ return array_merge($retval, array(
+ 'limit' => "How many total pages to return."
+ ));
}
public function getDescription() {
@@ -387,7 +399,6 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryBacklinks.php 30222 2008-01-28 19:05:26Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryBacklinks.php 37504 2008-07-10 14:28:09Z catrope $';
}
}
-
diff --git a/includes/api/ApiQueryBase.php b/includes/api/ApiQueryBase.php
index 031e3c02..f392186b 100644
--- a/includes/api/ApiQueryBase.php
+++ b/includes/api/ApiQueryBase.php
@@ -31,12 +31,12 @@ if (!defined('MEDIAWIKI')) {
/**
* This is a base class for all Query modules.
* It provides some common functionality such as constructing various SQL queries.
- *
- * @addtogroup API
+ *
+ * @ingroup API
*/
abstract class ApiQueryBase extends ApiBase {
- private $mQueryModule, $mDb, $tables, $where, $fields, $options;
+ private $mQueryModule, $mDb, $tables, $where, $fields, $options, $join_conds;
public function __construct($query, $moduleName, $paramPrefix = '') {
parent :: __construct($query->getMain(), $moduleName, $paramPrefix);
@@ -45,13 +45,22 @@ abstract class ApiQueryBase extends ApiBase {
$this->resetQueryParams();
}
+ /**
+ * Blank the internal arrays with query parameters
+ */
protected function resetQueryParams() {
$this->tables = array ();
$this->where = array ();
$this->fields = array ();
$this->options = array ();
+ $this->join_conds = array ();
}
+ /**
+ * Add a set of tables to the internal array
+ * @param mixed $tables Table name or array of table names
+ * @param mixed $alias Table alias, or null for no alias. Cannot be used with multiple tables
+ */
protected function addTables($tables, $alias = null) {
if (is_array($tables)) {
if (!is_null($alias))
@@ -59,11 +68,38 @@ abstract class ApiQueryBase extends ApiBase {
$this->tables = array_merge($this->tables, $tables);
} else {
if (!is_null($alias))
- $tables = $this->getDB()->tableName($tables) . ' ' . $alias;
+ $tables = $this->getAliasedName($tables, $alias);
$this->tables[] = $tables;
}
}
+
+ /**
+ * Get the SQL for a table name with alias
+ * @param string $table Table name
+ * @param string $alias Alias
+ * @return string SQL
+ */
+ protected function getAliasedName($table, $alias) {
+ return $this->getDB()->tableName($table) . ' ' . $alias;
+ }
+
+ /**
+ * Add a set of JOIN conditions to the internal array
+ *
+ * JOIN conditions are formatted as array( tablename => array(jointype, conditions)
+ * e.g. array('page' => array('LEFT JOIN', 'page_id=rev_page'))
+ * @param array $join_conds JOIN conditions
+ */
+ protected function addJoinConds($join_conds) {
+ if(!is_array($join_conds))
+ ApiBase::dieDebug(__METHOD__, 'Join conditions have to be arrays');
+ $this->join_conds = array_merge($this->join_conds, $join_conds);
+ }
+ /**
+ * Add a set of fields to select to the internal array
+ * @param mixed $value Field name or array of field names
+ */
protected function addFields($value) {
if (is_array($value))
$this->fields = array_merge($this->fields, $value);
@@ -71,6 +107,12 @@ abstract class ApiQueryBase extends ApiBase {
$this->fields[] = $value;
}
+ /**
+ * Same as addFields(), but add the fields only if a condition is met
+ * @param mixed $value See addFields()
+ * @param bool $condition If false, do nothing
+ * @return bool $condition
+ */
protected function addFieldsIf($value, $condition) {
if ($condition) {
$this->addFields($value);
@@ -79,6 +121,15 @@ abstract class ApiQueryBase extends ApiBase {
return false;
}
+ /**
+ * Add a set of WHERE clauses to the internal array.
+ * 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)
+ *
+ * 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);
@@ -86,6 +137,12 @@ abstract class ApiQueryBase extends ApiBase {
$this->where[] = $value;
}
+ /**
+ * Same as addWhere(), but add the WHERE clauses only if a condition is met
+ * @param mixed $value See addWhere()
+ * @param bool $condition If false, do nothing
+ * @return bool $condition
+ */
protected function addWhereIf($value, $condition) {
if ($condition) {
$this->addWhere($value);
@@ -94,11 +151,24 @@ abstract class ApiQueryBase extends ApiBase {
return false;
}
+ /**
+ * Equivalent to addWhere(array($field => $value))
+ * @param string $field Field name
+ * @param string $value Value; ignored if nul;
+ */
protected function addWhereFld($field, $value) {
if (!is_null($value))
$this->where[$field] = $value;
}
+ /**
+ * Add a WHERE clause corresponding to a range, and an ORDER BY
+ * clause to sort in the right direction
+ * @param string $field Field name
+ * @param string $dir If 'newer', sort in ascending order, otherwise sort in descending order
+ * @param string $start Value to start the list at. If $dir == 'newer' this is the lower boundary, otherwise it's the upper boundary
+ * @param string $end Value to end the list at. If $dir == 'newer' this is the upper boundary, otherwise it's the lower boundary
+ */
protected function addWhereRange($field, $dir, $start, $end) {
$isDirNewer = ($dir === 'newer');
$after = ($isDirNewer ? '>=' : '<=');
@@ -110,11 +180,19 @@ abstract class ApiQueryBase extends ApiBase {
if (!is_null($end))
$this->addWhere($field . $before . $db->addQuotes($end));
-
+
+ $order = $field . ($isDirNewer ? '' : ' DESC');
if (!isset($this->options['ORDER BY']))
- $this->addOption('ORDER BY', $field . ($isDirNewer ? '' : ' DESC'));
+ $this->addOption('ORDER BY', $order);
+ else
+ $this->addOption('ORDER BY', $this->options['ORDER BY'] . ', ' . $order);
}
+ /**
+ * Add an option such as LIMIT or USE INDEX
+ * @param string $name Option name
+ * @param string $value Option value
+ */
protected function addOption($name, $value = null) {
if (is_null($value))
$this->options[] = $name;
@@ -122,39 +200,71 @@ abstract class ApiQueryBase extends ApiBase {
$this->options[$name] = $value;
}
+ /**
+ * Execute a SELECT query based on the values in the internal arrays
+ * @param string $method Function the query should be attributed to. You should usually use __METHOD__ here
+ * @return ResultWrapper
+ */
protected function select($method) {
// getDB has its own profileDBIn/Out calls
$db = $this->getDB();
$this->profileDBIn();
- $res = $db->select($this->tables, $this->fields, $this->where, $method, $this->options);
+ $res = $db->select($this->tables, $this->fields, $this->where, $method, $this->options, $this->join_conds);
$this->profileDBOut();
return $res;
}
+ /**
+ * Estimate the row count for the SELECT query that would be run if we
+ * called select() right now, and check if it's acceptable.
+ * @return bool true if acceptable, false otherwise
+ */
+ protected function checkRowCount() {
+ $db = $this->getDB();
+ $this->profileDBIn();
+ $rowcount = $db->estimateRowCount($this->tables, $this->fields, $this->where, __METHOD__, $this->options);
+ $this->profileDBOut();
+
+ global $wgAPIMaxDBRows;
+ if($rowcount > $wgAPIMaxDBRows)
+ return false;
+ return true;
+ }
+
+ /**
+ * Add information (title and namespace) about a Title object to a result array
+ * @param array $arr Result array la ApiResult
+ * @param Title $title Title object
+ * @param string $prefix Module prefix
+ */
public static function addTitleInfo(&$arr, $title, $prefix='') {
$arr[$prefix . 'ns'] = intval($title->getNamespace());
$arr[$prefix . 'title'] = $title->getPrefixedText();
}
-
+
/**
* Override this method to request extra fields from the pageSet
* using $pageSet->requestField('fieldName')
+ * @param ApiPageSet $pageSet
*/
public function requestExtraData($pageSet) {
}
/**
* Get the main Query module
+ * @return ApiQuery
*/
public function getQuery() {
return $this->mQueryModule;
}
/**
- * Add sub-element under the page element with the given pageId.
+ * 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
*/
protected function addPageSubItems($pageId, $data) {
$result = $this->getResult();
@@ -164,19 +274,21 @@ abstract class ApiQueryBase extends ApiBase {
$data);
}
+ /**
+ * Set a query-continue value
+ * @param $paramName Parameter name
+ * @param $paramValue Parameter value
+ */
protected function setContinueEnumParameter($paramName, $paramValue) {
-
+
$paramName = $this->encodeParamName($paramName);
$msg = array( $paramName => $paramValue );
-
-// This is an alternative continue format as a part of the URL string
-// ApiResult :: setContent($msg, $paramName . '=' . urlencode($paramValue));
-
$this->getResult()->addValue('query-continue', $this->getModuleName(), $msg);
}
/**
* Get the Query database connection (readonly)
+ * @return Database
*/
protected function getDB() {
if (is_null($this->mDb))
@@ -186,57 +298,62 @@ abstract class ApiQueryBase extends ApiBase {
/**
* Selects the query database connection with the given name.
- * If no such connection has been requested before, it will be created.
- * Subsequent calls with the same $name will return the same connection
- * as the first, regardless of $db or $groups new values.
+ * If no such connection has been requested before, it will be created.
+ * Subsequent calls with the same $name will return the same connection
+ * as the first, regardless of $db or $groups new values.
+ * @param string $name Name to assign to the database connection
+ * @param int $db One of the DB_* constants
+ * @param array $groups Query groups
+ * @return Database
*/
public function selectNamedDB($name, $db, $groups) {
- $this->mDb = $this->getQuery()->getNamedDB($name, $db, $groups);
+ $this->mDb = $this->getQuery()->getNamedDB($name, $db, $groups);
}
/**
* Get the PageSet object to work on
- * @return ApiPageSet data
+ * @return ApiPageSet
*/
protected function getPageSet() {
return $this->getQuery()->getPageSet();
}
/**
- * This is a very simplistic utility function
- * to convert a non-namespaced title string to a db key.
- * It will replace all ' ' with '_'
+ * Convert a title to a DB key
+ * @param string $title Page title with spaces
+ * @return string Page title with underscores
*/
- public static function titleToKey($title) {
- return str_replace(' ', '_', $title);
+ public function titleToKey($title) {
+ $t = Title::newFromText($title);
+ if(!$t)
+ $this->dieUsageMsg(array('invalidtitle', $title));
+ return $t->getDbKey();
}
- public static function keyToTitle($key) {
- return str_replace('_', ' ', $key);
+ /**
+ * The inverse of titleToKey()
+ * @param string $key Page title with underscores
+ * @return string Page title with spaces
+ */
+ public function keyToTitle($key) {
+ $t = Title::newFromDbKey($key);
+ # This really shouldn't happen but we gotta check anyway
+ if(!$t)
+ $this->dieUsageMsg(array('invalidtitle', $key));
+ return $t->getPrefixedText();
}
- public function getTokenFlag($tokenArr, $action) {
- if ($this->getMain()->getRequest()->getVal('callback') !== null) {
- // Don't do any session-specific data.
- return false;
- }
- if (in_array($action, $tokenArr)) {
- global $wgUser;
- if ($wgUser->isAllowed($action))
- return true;
- else
- $this->dieUsage("Action '$action' is not allowed for the current user", 'permissiondenied');
- }
- return false;
- }
-
+ /**
+ * Get version string for use in the API help output
+ * @return string
+ */
public static function getBaseVersion() {
- return __CLASS__ . ': $Id: ApiQueryBase.php 31484 2008-03-03 05:46:20Z brion $';
+ return __CLASS__ . ': $Id: ApiQueryBase.php 37083 2008-07-05 11:18:50Z catrope $';
}
}
/**
- * @addtogroup API
+ * @ingroup API
*/
abstract class ApiQueryGeneratorBase extends ApiQueryBase {
@@ -247,6 +364,10 @@ abstract class ApiQueryGeneratorBase extends ApiQueryBase {
$this->mIsGenerator = false;
}
+ /**
+ * Switch this module to generator mode. By default, generator mode is
+ * switched off and the module acts like a normal query module.
+ */
public function setGeneratorMode() {
$this->mIsGenerator = true;
}
@@ -267,4 +388,3 @@ abstract class ApiQueryGeneratorBase extends ApiQueryBase {
*/
public abstract function executeGenerator($resultPageSet);
}
-
diff --git a/includes/api/ApiQueryBlocks.php b/includes/api/ApiQueryBlocks.php
index 165792b5..ebe87908 100644
--- a/includes/api/ApiQueryBlocks.php
+++ b/includes/api/ApiQueryBlocks.php
@@ -30,10 +30,12 @@ if (!defined('MEDIAWIKI')) {
/**
* Query module to enumerate all available pages.
- *
- * @addtogroup API
+ *
+ * @ingroup API
*/
class ApiQueryBlocks extends ApiQueryBase {
+
+ var $users;
public function __construct($query, $moduleName) {
parent :: __construct($query, $moduleName, 'bk');
@@ -47,6 +49,9 @@ class ApiQueryBlocks extends ApiQueryBase {
global $wgUser;
$params = $this->extractRequestParams();
+ if(isset($params['users']) && isset($params['ip']))
+ $this->dieUsage('bkusers and bkip cannot be used together', 'usersandip');
+
$prop = array_flip($params['prop']);
$fld_id = isset($prop['id']);
$fld_user = isset($prop['user']);
@@ -66,7 +71,7 @@ class ApiQueryBlocks extends ApiQueryBase {
if($fld_id)
$this->addFields('ipb_id');
if($fld_user)
- $this->addFields(array('ipb_address', 'ipb_user'));
+ $this->addFields(array('ipb_address', 'ipb_user', 'ipb_auto'));
if($fld_by)
{
$this->addTables('user');
@@ -89,8 +94,32 @@ class ApiQueryBlocks extends ApiQueryBase {
if(isset($params['ids']))
$this->addWhere(array('ipb_id' => $params['ids']));
if(isset($params['users']))
- $this->addWhere(array('ipb_address' => $params['users']));
- if(!$wgUser->isAllowed('oversight'))
+ {
+ foreach((array)$params['users'] as $u)
+ $this->prepareUsername($u);
+ $this->addWhere(array('ipb_address' => $this->usernames));
+ }
+ if(isset($params['ip']))
+ {
+ list($ip, $range) = IP::parseCIDR($params['ip']);
+ if($ip && $range)
+ {
+ # We got a CIDR range
+ if($range < 16)
+ $this->dieUsage('CIDR ranges broader than /16 are not accepted', 'cidrtoobroad');
+ $lower = wfBaseConvert($ip, 10, 16, 8, false);
+ $upper = wfBaseConvert($ip + pow(2, 32 - $range) - 1, 10, 16, 8, false);
+ }
+ else
+ $lower = $upper = IP::toHex($params['ip']);
+ $prefix = substr($lower, 0, 4);
+ $this->addWhere(array(
+ "ipb_range_start LIKE '$prefix%'",
+ "ipb_range_start <= '$lower'",
+ "ipb_range_end >= '$upper'"
+ ));
+ }
+ if(!$wgUser->isAllowed('suppress'))
$this->addWhere(array('ipb_deleted' => 0));
// Purge expired entries on one in every 10 queries
@@ -152,6 +181,18 @@ class ApiQueryBlocks extends ApiQueryBase {
$result->setIndexedTagName($data, 'block');
$result->addValue('query', $this->getModuleName(), $data);
}
+
+ protected function prepareUsername($user)
+ {
+ if(!$user)
+ $this->dieUsage('User parameter may not be empty', 'param_user');
+ $name = User::isIP($user)
+ ? $user
+ : User::getCanonicalName($user, 'valid');
+ if($name === false)
+ $this->dieUsage("User name {$user} is not valid", 'param_user');
+ $this->usernames[] = $name;
+ }
protected function convertHexIP($ip)
{
@@ -188,6 +229,7 @@ class ApiQueryBlocks extends ApiQueryBase {
'users' => array(
ApiBase :: PARAM_ISMULTI => true
),
+ 'ip' => null,
'limit' => array(
ApiBase :: PARAM_DFLT => 10,
ApiBase :: PARAM_TYPE => 'limit',
@@ -219,6 +261,8 @@ class ApiQueryBlocks extends ApiQueryBase {
'dir' => 'The direction in which to enumerate',
'ids' => 'Pipe-separated list of block IDs to list (optional)',
'users' => 'Pipe-separated list of users to search for (optional)',
+ 'ip' => array( 'Get all blocks applying to this IP or CIDR range, including range blocks.',
+ 'Cannot be used together with bkusers. CIDR ranges broader than /16 are not accepted.'),
'limit' => 'The maximum amount of blocks to list',
'prop' => 'Which properties to get',
);
@@ -229,11 +273,12 @@ class ApiQueryBlocks extends ApiQueryBase {
}
protected function getExamples() {
- return array (
+ return array ( 'api.php?action=query&list=blocks',
+ 'api.php?action=query&list=blocks&bkusers=Alice|Bob'
);
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryBlocks.php 30222 2008-01-28 19:05:26Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryBlocks.php 37892 2008-07-21 21:37:11Z catrope $';
}
}
diff --git a/includes/api/ApiQueryCategories.php b/includes/api/ApiQueryCategories.php
index 63d42bfa..51492d63 100644
--- a/includes/api/ApiQueryCategories.php
+++ b/includes/api/ApiQueryCategories.php
@@ -30,8 +30,8 @@ if (!defined('MEDIAWIKI')) {
/**
* A query module to enumerate categories the set of pages belong to.
- *
- * @addtogroup API
+ *
+ * @ingroup API
*/
class ApiQueryCategories extends ApiQueryGeneratorBase {
@@ -59,8 +59,8 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
'cl_from',
'cl_to'
));
-
- $fld_sortkey = false;
+
+ $fld_sortkey = $fld_timestamp = false;
if (!is_null($prop)) {
foreach($prop as $p) {
switch ($p) {
@@ -68,24 +68,51 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
$this->addFields('cl_sortkey');
$fld_sortkey = true;
break;
+ case 'timestamp':
+ $this->addFields('cl_timestamp');
+ $fld_timestamp = true;
+ break;
default :
ApiBase :: dieDebug(__METHOD__, "Unknown prop=$p");
}
}
}
-
+
$this->addTables('categorylinks');
$this->addWhereFld('cl_from', array_keys($this->getPageSet()->getGoodTitles()));
- $this->addOption('ORDER BY', "cl_from, cl_to");
+ if(!is_null($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");
+ $clfrom = intval($cont[0]);
+ $clto = $this->getDb()->strencode($this->titleToKey($cont[1]));
+ $this->addWhere("cl_from > $clfrom OR ".
+ "(cl_from = $clfrom AND ".
+ "cl_to >= '$clto')");
+ }
+ # 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');
+ else
+ $this->addOption('ORDER BY', "cl_from, cl_to");
$db = $this->getDB();
$res = $this->select(__METHOD__);
if (is_null($resultPageSet)) {
-
+
$data = array();
- $lastId = 0; // database has no ID 0
+ $lastId = 0; // database has no ID 0
+ $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->cl_from .
+ '|' . $this->keyToTitle($row->cl_to));
+ break;
+ }
if ($lastId != $row->cl_from) {
if($lastId != 0) {
$this->addPageSubItems($lastId, $data);
@@ -93,13 +120,15 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
}
$lastId = $row->cl_from;
}
-
+
$title = Title :: makeTitle(NS_CATEGORY, $row->cl_to);
-
+
$vals = array();
ApiQueryBase :: addTitleInfo($vals, $title);
if ($fld_sortkey)
$vals['sortkey'] = $row->cl_sortkey;
+ if ($fld_timestamp)
+ $vals['timestamp'] = $row->cl_timestamp;
$data[] = $vals;
}
@@ -112,6 +141,14 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
$titles = array();
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->cl_from .
+ '|' . $this->keyToTitle($row->cl_to));
+ break;
+ }
+
$titles[] = Title :: makeTitle(NS_CATEGORY, $row->cl_to);
}
$resultPageSet->populateFromTitles($titles);
@@ -126,14 +163,25 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
ApiBase :: PARAM_ISMULTI => true,
ApiBase :: PARAM_TYPE => array (
'sortkey',
+ 'timestamp',
)
- )
+ ),
+ '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 (
'prop' => 'Which additional properties to get for each category.',
+ 'limit' => 'How many categories to return',
+ 'continue' => 'When more results are available, use this to continue',
);
}
@@ -151,7 +199,6 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryCategories.php 30222 2008-01-28 19:05:26Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryCategories.php 37909 2008-07-22 13:26:15Z catrope $';
}
}
-
diff --git a/includes/api/ApiQueryCategoryInfo.php b/includes/api/ApiQueryCategoryInfo.php
new file mode 100644
index 00000000..f809bb15
--- /dev/null
+++ b/includes/api/ApiQueryCategoryInfo.php
@@ -0,0 +1,91 @@
+<?php
+
+/*
+ * Created on May 13, 2007
+ *
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+ // Eclipse helper - will be ignored in production
+ require_once ("ApiQueryBase.php");
+}
+
+/**
+ * This query adds <categories> subelement to all pages with the list of images embedded into those pages.
+ *
+ * @ingroup API
+ */
+class ApiQueryCategoryInfo extends ApiQueryBase {
+
+ public function __construct($query, $moduleName) {
+ parent :: __construct($query, $moduleName, 'ci');
+ }
+
+ public function execute() {
+ $alltitles = $this->getPageSet()->getAllTitlesByNamespace();
+ $categories = $alltitles[NS_CATEGORY];
+ if(empty($categories))
+ return;
+
+ $titles = $this->getPageSet()->getGoodTitles() +
+ $this->getPageSet()->getMissingTitles();
+ $cattitles = array();
+ foreach($categories as $c)
+ {
+ $t = $titles[$c];
+ $cattitles[$c] = $t->getDbKey();
+ }
+
+ $this->addTables('category');
+ $this->addFields(array('cat_title', 'cat_pages', 'cat_subcats', 'cat_files', 'cat_hidden'));
+ $this->addWhere(array('cat_title' => $cattitles));
+
+ $db = $this->getDB();
+ $res = $this->select(__METHOD__);
+
+ $data = array();
+ $catids = array_flip($cattitles);
+ while($row = $db->fetchObject($res))
+ {
+ $vals = array();
+ $vals['size'] = $row->cat_pages;
+ $vals['pages'] = $row->cat_pages - $row->cat_subcats - $row->cat_files;
+ $vals['files'] = $row->cat_files;
+ $vals['subcats'] = $row->cat_subcats;
+ if($row->cat_hidden)
+ $vals['hidden'] = '';
+ $this->addPageSubItems($catids[$row->cat_title], $vals);
+ }
+ $db->freeResult($res);
+ }
+
+ public function getDescription() {
+ return 'Returns information about the given categories';
+ }
+
+ protected function getExamples() {
+ return "api.php?action=query&prop=categoryinfo&titles=Category:Foo|Category:Bar";
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiQueryCategoryInfo.php 37504 2008-07-10 14:28:09Z catrope $';
+ }
+}
diff --git a/includes/api/ApiQueryCategoryMembers.php b/includes/api/ApiQueryCategoryMembers.php
index e831f291..3909b213 100644
--- a/includes/api/ApiQueryCategoryMembers.php
+++ b/includes/api/ApiQueryCategoryMembers.php
@@ -30,8 +30,8 @@ if (!defined('MEDIAWIKI')) {
/**
* A query module to enumerate pages that belong to a category.
- *
- * @addtogroup API
+ *
+ * @ingroup API
*/
class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
@@ -51,19 +51,13 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
$params = $this->extractRequestParams();
- if (is_null($params['category'])) {
- if (is_null($params['title']))
- $this->dieUsage("Either the cmcategory or the cmtitle parameter is required", 'notitle');
- else
- $categoryTitle = Title::newFromText($params['title']);
- } else if(is_null($params['title']))
- $categoryTitle = Title::makeTitleSafe(NS_CATEGORY, $params['category']);
- else
- $this->dieUsage("The cmcategory and cmtitle parameters can't be used together", 'titleandcategory');
+ if ( !isset($params['title']) || is_null($params['title']) )
+ $this->dieUsage("The cmtitle parameter is required", 'notitle');
+ $categoryTitle = Title::newFromText($params['title']);
if ( is_null( $categoryTitle ) || $categoryTitle->getNamespace() != NS_CATEGORY )
$this->dieUsage("The category name you entered is not valid", 'invalidcategory');
-
+
$prop = array_flip($params['prop']);
$fld_ids = isset($prop['ids']);
$fld_title = isset($prop['title']);
@@ -78,26 +72,29 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
$this->addFields(array('cl_from', 'cl_sortkey'));
}
- $this->addFieldsIf('cl_timestamp', $fld_timestamp);
- $this->addTables(array('page','categorylinks')); // must be in this order for 'USE INDEX'
+ $this->addFieldsIf('cl_timestamp', $fld_timestamp || $params['sort'] == 'timestamp');
+ $this->addTables(array('page','categorylinks')); // must be in this order for 'USE INDEX'
// Not needed after bug 10280 is applied to servers
if($params['sort'] == 'timestamp')
{
$this->addOption('USE INDEX', 'cl_timestamp');
- $this->addOption('ORDER BY', 'cl_to, cl_timestamp' . ($params['dir'] == 'desc' ? ' DESC' : ''));
+ // 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' . ($params['dir'] == 'desc' ? ' DESC' : '') . ', cl_from');
+ $this->addOption('ORDER BY', 'cl_to, cl_sortkey' . $dir . ', cl_from' . $dir);
}
$this->addWhere('cl_from=page_id');
- $this->setContinuation($params['continue']);
+ $this->setContinuation($params['continue'], $params['dir']);
$this->addWhereFld('cl_to', $categoryTitle->getDBkey());
$this->addWhereFld('page_namespace', $params['namespace']);
- $this->addWhereRange('cl_timestamp', ($params['dir'] == 'asc' ? 'newer' : 'older'), $params['start'], $params['end']);
-
+ if($params['sort'] == 'timestamp')
+ $this->addWhereRange('cl_timestamp', ($params['dir'] == 'asc' ? 'newer' : 'older'), $params['start'], $params['end']);
+
$limit = $params['limit'];
$this->addOption('LIMIT', $limit +1);
@@ -111,16 +108,19 @@ class ApiQueryCategoryMembers 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->getContinueStr($row, $lastSortKey));
+ if ($params['sort'] == 'timestamp')
+ $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->cl_timestamp));
+ else
+ $this->setContinueEnumParameter('continue', $this->getContinueStr($row, $lastSortKey));
break;
}
- $lastSortKey = $row->cl_sortkey; // detect duplicate sortkeys
-
+ $lastSortKey = $row->cl_sortkey; // detect duplicate sortkeys
+
if (is_null($resultPageSet)) {
$vals = array();
if ($fld_ids)
- $vals['pageid'] = intval($row->page_id);
+ $vals['pageid'] = intval($row->page_id);
if ($fld_title) {
$title = Title :: makeTitle($row->page_namespace, $row->page_title);
$vals['ns'] = intval($title->getNamespace());
@@ -142,47 +142,48 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
$this->getResult()->addValue('query', $this->getModuleName(), $data);
}
}
-
+
private function getContinueStr($row, $lastSortKey) {
$ret = $row->cl_sortkey . '|';
if ($row->cl_sortkey == $lastSortKey) // duplicate sort key, add cl_from
$ret .= $row->cl_from;
return $ret;
}
-
+
/**
- * Add DB WHERE clause to continue previous query based on 'continue' parameter
+ * Add DB WHERE clause to continue previous query based on 'continue' parameter
*/
- private function setContinuation($continue) {
+ private function setContinuation($continue, $dir) {
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);
+ $hasError = ($from == 0);
}
-
+
if ($hasError)
$this->dieUsage("Invalid continue param. You should pass the original value returned by the previous query", "badcontinue");
$encSortKey = $this->getDB()->addQuotes($continueList[0]);
$encFrom = $this->getDB()->addQuotes($from);
+
+ $op = ($dir == 'desc' ? '<' : '>');
if ($from != 0) {
// Duplicate sort key continue
- $this->addWhere( "cl_sortkey>$encSortKey OR (cl_sortkey=$encSortKey AND cl_from>=$encFrom)" );
+ $this->addWhere( "cl_sortkey$op$encSortKey OR (cl_sortkey=$encSortKey AND cl_from$op=$encFrom)" );
} else {
- $this->addWhere( "cl_sortkey>=$encSortKey" );
+ $this->addWhere( "cl_sortkey$op=$encSortKey" );
}
}
public function getAllowedParams() {
return array (
'title' => null,
- 'category' => null, // DEPRECATED, will be removed in early March
'prop' => array (
ApiBase :: PARAM_DFLT => 'ids|title',
ApiBase :: PARAM_ISMULTI => true,
@@ -235,11 +236,10 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
'namespace' => 'Only include pages in these namespaces',
'sort' => 'Property to sort by',
'dir' => 'In which direction to sort',
- 'start' => 'Timestamp to start listing from',
- 'end' => 'Timestamp to end listing at',
+ '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',
'continue' => 'For large categories, give the value retured from previous query',
'limit' => 'The maximum number of pages to return.',
- 'category' => 'DEPRECATED. Like title, but without the Category: prefix.',
);
}
@@ -257,7 +257,6 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryCategoryMembers.php 30670 2008-02-07 15:17:42Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryCategoryMembers.php 35098 2008-05-20 17:13:28Z ialex $';
}
}
-
diff --git a/includes/api/ApiQueryDeletedrevs.php b/includes/api/ApiQueryDeletedrevs.php
index 1b7fbdb0..8368896d 100644
--- a/includes/api/ApiQueryDeletedrevs.php
+++ b/includes/api/ApiQueryDeletedrevs.php
@@ -30,8 +30,8 @@ if (!defined('MEDIAWIKI')) {
/**
* Query module to enumerate all available pages.
- *
- * @addtogroup API
+ *
+ * @ingroup API
*/
class ApiQueryDeletedrevs extends ApiQueryBase {
@@ -87,11 +87,16 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
// Check limits
$userMax = $fld_content ? ApiBase :: LIMIT_SML1 : ApiBase :: LIMIT_BIG1;
$botMax = $fld_content ? ApiBase :: LIMIT_SML2 : ApiBase :: LIMIT_BIG2;
+
+ $limit = $params['limit'];
+
if( $limit == 'max' ) {
$limit = $this->getMain()->canApiHighLimits() ? $botMax : $userMax;
- $this->getResult()->addValue( 'limits', 'limit', $limit );
+ $this->getResult()->addValue( 'limits', $this->getModuleName(), $limit );
}
- $this->validateLimit('limit', $params['limit'], 1, $userMax, $botMax);
+
+ $this->validateLimit('limit', $limit, 1, $userMax, $botMax);
+
if($fld_token)
// Undelete tokens are identical for all pages, so we cache one here
$token = $wgUser->editToken();
@@ -104,17 +109,15 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
$this->addWhere($where);
}
- $this->addOption('LIMIT', $params['limit'] + 1);
+ $this->addOption('LIMIT', $limit + 1);
$this->addWhereRange('ar_timestamp', $params['dir'], $params['start'], $params['end']);
- if(isset($params['namespace']))
- $this->addWhereFld('ar_namespace', $params['namespace']);
$res = $this->select(__METHOD__);
$pages = array();
$count = 0;
// First populate the $pages array
while($row = $db->fetchObject($res))
{
- if($count++ == $params['limit'])
+ if(++$count > $limit)
{
// We've had enough
$this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->ar_timestamp));
@@ -178,10 +181,6 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
),
ApiBase :: PARAM_DFLT => 'older'
),
- 'namespace' => array(
- ApiBase :: PARAM_ISMULTI => true,
- ApiBase :: PARAM_TYPE => 'namespace'
- ),
'limit' => array(
ApiBase :: PARAM_DFLT => 10,
ApiBase :: PARAM_TYPE => 'limit',
@@ -210,7 +209,6 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
'start' => 'The timestamp to start enumerating from',
'end' => 'The timestamp to stop enumerating at',
'dir' => 'The direction in which to enumerate',
- 'namespace' => 'The namespaces to search in',
'limit' => 'The maximum amount of revisions to list',
'prop' => 'Which properties to get'
);
@@ -222,14 +220,14 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
protected function getExamples() {
return array (
- 'List the first 50 deleted revisions in the Category and Category talk namespaces',
- ' api.php?action=query&list=deletedrevs&drdir=newer&drlimit=50&drnamespace=14|15',
+ 'List the first 50 deleted revisions',
+ ' api.php?action=query&list=deletedrevs&drdir=newer&drlimit=50',
'List the last deleted revisions of Main Page and Talk:Main Page, with content:',
' api.php?action=query&list=deletedrevs&titles=Main%20Page|Talk:Main%20Page&drprop=user|comment|content'
);
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryDeletedrevs.php 30222 2008-01-28 19:05:26Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryDeletedrevs.php 37502 2008-07-10 14:13:11Z catrope $';
}
}
diff --git a/includes/api/ApiQueryExtLinksUsage.php b/includes/api/ApiQueryExtLinksUsage.php
index 896a0171..8ffb7246 100644
--- a/includes/api/ApiQueryExtLinksUsage.php
+++ b/includes/api/ApiQueryExtLinksUsage.php
@@ -29,7 +29,7 @@ if (!defined('MEDIAWIKI')) {
}
/**
- * @addtogroup API
+ * @ingroup API
*/
class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase {
@@ -51,43 +51,53 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase {
$protocol = $params['protocol'];
$query = $params['query'];
- if (is_null($query))
- $this->dieUsage('Missing required query parameter', 'params');
-
+
// Find the right prefix
global $wgUrlProtocols;
- foreach ($wgUrlProtocols as $p) {
- if( substr( $p, 0, strlen( $protocol ) ) === $protocol ) {
- $protocol = $p;
- break;
+ if(!is_null($protocol) && !empty($protocol) && !in_array($protocol, $wgUrlProtocols))
+ {
+ foreach ($wgUrlProtocols as $p) {
+ if( substr( $p, 0, strlen( $protocol ) ) === $protocol ) {
+ $protocol = $p;
+ break;
+ }
}
}
-
- $likeQuery = LinkFilter::makeLike($query , $protocol);
- if (!$likeQuery)
- $this->dieUsage('Invalid query', 'bad_query');
- $likeQuery = substr($likeQuery, 0, strpos($likeQuery,'%')+1);
+ else
+ $protocol = null;
- $this->addTables(array('page','externallinks')); // must be in this order for 'USE INDEX'
+ $db = $this->getDb();
+ $this->addTables(array('page','externallinks')); // must be in this order for 'USE INDEX'
$this->addOption('USE INDEX', 'el_index');
-
- $db = $this->getDB();
$this->addWhere('page_id=el_from');
- $this->addWhere('el_index LIKE ' . $db->addQuotes( $likeQuery ));
$this->addWhereFld('page_namespace', $params['namespace']);
+ if(!is_null($query) || $query != '')
+ {
+ if(is_null($protocol))
+ $protocol = 'http://';
+
+ $likeQuery = LinkFilter::makeLike($query, $protocol);
+ if (!$likeQuery)
+ $this->dieUsage('Invalid query', 'bad_query');
+ $likeQuery = substr($likeQuery, 0, strpos($likeQuery,'%')+1);
+ $this->addWhere('el_index LIKE ' . $db->addQuotes( $likeQuery ));
+ }
+ else if(!is_null($protocol))
+ $this->addWhere('el_index LIKE ' . $db->addQuotes( "$protocol%" ));
+
$prop = array_flip($params['prop']);
$fld_ids = isset($prop['ids']);
$fld_title = isset($prop['title']);
$fld_url = isset($prop['url']);
-
+
if (is_null($resultPageSet)) {
$this->addFields(array (
'page_id',
'page_namespace',
'page_title'
));
- $this->addFieldsIf('el_to', $fld_url);
+ $this->addFieldsIf('el_to', $fld_url);
} else {
$this->addFields($resultPageSet->getPageTableFields());
}
@@ -105,7 +115,7 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase {
while ($row = $db->fetchObject($res)) {
if (++ $count > $limit) {
// We've reached the one extra which shows that there are additional pages to be had. Stop here...
- $this->setContinueEnumParameter('offset', $offset+$limit+1);
+ $this->setContinueEnumParameter('offset', $offset+$limit);
break;
}
@@ -136,11 +146,11 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase {
public function getAllowedParams() {
global $wgUrlProtocols;
- $protocols = array();
+ $protocols = array('');
foreach ($wgUrlProtocols as $p) {
$protocols[] = substr($p, 0, strpos($p,':'));
}
-
+
return array (
'prop' => array (
ApiBase :: PARAM_ISMULTI => true,
@@ -156,7 +166,7 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase {
),
'protocol' => array (
ApiBase :: PARAM_TYPE => $protocols,
- ApiBase :: PARAM_DFLT => 'http',
+ ApiBase :: PARAM_DFLT => '',
),
'query' => null,
'namespace' => array (
@@ -177,10 +187,11 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase {
return array (
'prop' => 'What pieces of information to include',
'offset' => 'Used for paging. Use the value returned for "continue"',
- 'protocol' => 'Protocol of the url',
- 'query' => 'Search string without protocol. See [[Special:LinkSearch]]',
+ 'protocol' => array( 'Protocol of the url. If empty and euquery set, the protocol is http.',
+ 'Leave both this and euquery empty to list all external links'),
+ 'query' => 'Search string without protocol. See [[Special:LinkSearch]]. Leave empty to list all external links',
'namespace' => 'The page namespace(s) to enumerate.',
- 'limit' => 'How many entries to return.'
+ 'limit' => 'How many pages to return.'
);
}
@@ -195,6 +206,6 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryExtLinksUsage.php 30222 2008-01-28 19:05:26Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryExtLinksUsage.php 37909 2008-07-22 13:26:15Z catrope $';
}
}
diff --git a/includes/api/ApiQueryExternalLinks.php b/includes/api/ApiQueryExternalLinks.php
index 07183910..a24f15d8 100644
--- a/includes/api/ApiQueryExternalLinks.php
+++ b/includes/api/ApiQueryExternalLinks.php
@@ -30,8 +30,8 @@ if (!defined('MEDIAWIKI')) {
/**
* A query module to list all external URLs found on a given set of pages.
- *
- * @addtogroup API
+ *
+ * @ingroup API
*/
class ApiQueryExternalLinks extends ApiQueryBase {
@@ -40,21 +40,37 @@ class ApiQueryExternalLinks extends ApiQueryBase {
}
public function execute() {
+ if ( $this->getPageSet()->getGoodTitleCount() == 0 )
+ return;
+ $params = $this->extractRequestParams();
$this->addFields(array (
'el_from',
'el_to'
));
-
+
$this->addTables('externallinks');
$this->addWhereFld('el_from', array_keys($this->getPageSet()->getGoodTitles()));
+ # Don't order by el_from if it's constant in the WHERE clause
+ if(count($this->getPageSet()->getGoodTitles()) != 1)
+ $this->addOption('ORDER BY', 'el_from');
+ $this->addOption('LIMIT', $params['limit'] + 1);
+ if(!is_null($params['offset']))
+ $this->addOption('OFFSET', $params['offset']);
$db = $this->getDB();
$res = $this->select(__METHOD__);
-
+
$data = array();
- $lastId = 0; // database has no ID 0
+ $lastId = 0; // database has no ID 0
+ $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('offset', @$params['offset'] + $params['limit']);
+ break;
+ }
if ($lastId != $row->el_from) {
if($lastId != 0) {
$this->addPageSubItems($lastId, $data);
@@ -62,7 +78,7 @@ class ApiQueryExternalLinks extends ApiQueryBase {
}
$lastId = $row->el_from;
}
-
+
$entry = array();
ApiResult :: setContent($entry, $row->el_to);
$data[] = $entry;
@@ -75,6 +91,26 @@ class ApiQueryExternalLinks extends ApiQueryBase {
$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
+ ),
+ 'offset' => null,
+ );
+ }
+
+ public function getParamDescription () {
+ return array(
+ 'limit' => 'How many links to return',
+ 'offset' => 'When more results are available, use this to continue',
+ );
+ }
+
public function getDescription() {
return 'Returns all external urls (not interwikies) from the given page(s)';
}
@@ -87,7 +123,6 @@ class ApiQueryExternalLinks extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryExternalLinks.php 30222 2008-01-28 19:05:26Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryExternalLinks.php 37270 2008-07-07 17:32:22Z catrope $';
}
}
-
diff --git a/includes/api/ApiQueryImageInfo.php b/includes/api/ApiQueryImageInfo.php
index 3714ccf6..33ff1d3f 100644
--- a/includes/api/ApiQueryImageInfo.php
+++ b/includes/api/ApiQueryImageInfo.php
@@ -30,8 +30,8 @@ if (!defined('MEDIAWIKI')) {
/**
* A query action to get image information and upload history.
- *
- * @addtogroup API
+ *
+ * @ingroup API
*/
class ApiQueryImageInfo extends ApiQueryBase {
@@ -42,65 +42,63 @@ class ApiQueryImageInfo extends ApiQueryBase {
public function execute() {
$params = $this->extractRequestParams();
- $prop = array_flip($params['prop']);
- $this->fld_timestamp = isset($prop['timestamp']);
- $this->fld_user = isset($prop['user']);
- $this->fld_comment = isset($prop['comment']);
- $this->fld_url = isset($prop['url']);
- $this->fld_size = isset($prop['size']);
- $this->fld_sha1 = isset($prop['sha1']);
- $this->fld_metadata = isset($prop['metadata']);
-
+ $prop = array_flip($params['prop']);
+
if($params['urlheight'] != -1 && $params['urlwidth'] == -1)
$this->dieUsage("iiurlheight cannot be used without iiurlwidth", 'iiurlwidth');
- $this->scale = ($params['urlwidth'] != -1);
- $this->urlwidth = $params['urlwidth'];
- $this->urlheight = $params['urlheight'];
+
+ if ( $params['urlwidth'] != -1 ) {
+ $scale = array();
+ $scale['width'] = $params['urlwidth'];
+ $scale['height'] = $params['urlheight'];
+ } else {
+ $scale = null;
+ }
$pageIds = $this->getPageSet()->getAllTitlesByNamespace();
if (!empty($pageIds[NS_IMAGE])) {
- foreach ($pageIds[NS_IMAGE] as $dbKey => $pageId) {
-
- $title = Title :: makeTitle(NS_IMAGE, $dbKey);
- $img = wfFindFile($title);
-
+
+ $result = $this->getResult();
+ $images = RepoGroup::singleton()->findFiles( array_keys( $pageIds[NS_IMAGE] ) );
+ foreach ( $images as $img ) {
$data = array();
- if ( !$img ) {
- $repository = '';
- } else {
-
- $repository = $img->getRepoName();
-
- // Get information about the current version first
- // Check that the current version is within the start-end boundaries
- if((is_null($params['start']) || $img->getTimestamp() <= $params['start']) &&
- (is_null($params['end']) || $img->getTimestamp() >= $params['end'])) {
- $data[] = $this->getInfo($img);
- }
-
- // Now get the old revisions
- // Get one more to facilitate query-continue functionality
- $count = count($data);
- $oldies = $img->getHistory($params['limit'] - $count + 1, $params['start'], $params['end']);
- foreach($oldies as $oldie) {
- 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)
- $this->setContinueEnumParameter('start', $oldie->getTimestamp());
- break;
- }
- $data[] = $this->getInfo($oldie);
+
+ // Get information about the current version first
+ // Check that the current version is within the start-end boundaries
+ if((is_null($params['start']) || $img->getTimestamp() <= $params['start']) &&
+ (is_null($params['end']) || $img->getTimestamp() >= $params['end'])) {
+ $data[] = self::getInfo( $img, $prop, $result, $scale );
+ }
+
+ // Now get the old revisions
+ // Get one more to facilitate query-continue functionality
+ $count = count($data);
+ $oldies = $img->getHistory($params['limit'] - $count + 1, $params['start'], $params['end']);
+ foreach($oldies as $oldie) {
+ 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)
+ $this->setContinueEnumParameter('start', $oldie->getTimestamp());
+ break;
}
+ $data[] = self::getInfo( $oldie, $prop, $result );
}
- $this->getResult()->addValue(array(
- 'query', 'pages', intval($pageId)),
- 'imagerepository', $repository
+ $pageId = $pageIds[NS_IMAGE][ $img->getOriginalTitle()->getDBkey() ];
+ $result->addValue(
+ array( 'query', 'pages', intval( $pageId ) ),
+ 'imagerepository', $img->getRepoName()
);
- if (!empty($data))
- $this->addPageSubItems($pageId, $data);
+ $this->addPageSubItems($pageId, $data);
}
+
+ $missing = array_diff( array_keys( $pageIds[NS_IMAGE] ), array_keys( $images ) );
+ foreach ( $missing as $title )
+ $result->addValue(
+ array( 'query', 'pages', intval( $pageIds[NS_IMAGE][$title] ) ),
+ 'imagerepository', ''
+ );
}
}
@@ -109,41 +107,48 @@ class ApiQueryImageInfo extends ApiQueryBase {
* @param File f The image
* @return array Result array
*/
- protected function getInfo($f) {
+ static function getInfo($file, $prop, $result, $scale = null) {
$vals = array();
- if($this->fld_timestamp)
- $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $f->getTimestamp());
- if($this->fld_user) {
- $vals['user'] = $f->getUser();
- if(!$f->getUser('id'))
+ if( isset( $prop['timestamp'] ) )
+ $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $file->getTimestamp());
+ if( isset( $prop['user'] ) ) {
+ $vals['user'] = $file->getUser();
+ if( !$file->getUser( 'id' ) )
$vals['anon'] = '';
}
- if($this->fld_size) {
- $vals['size'] = intval($f->getSize());
- $vals['width'] = intval($f->getWidth());
- $vals['height'] = intval($f->getHeight());
+ if( isset( $prop['size'] ) || isset( $prop['dimensions'] ) ) {
+ $vals['size'] = intval( $file->getSize() );
+ $vals['width'] = intval( $file->getWidth() );
+ $vals['height'] = intval( $file->getHeight() );
}
- if($this->fld_url) {
- if($this->scale && !$f->isOld()) {
- $thumb = $f->getThumbnail($this->urlwidth, $this->urlheight);
- if($thumb)
+ if( isset( $prop['url'] ) ) {
+ if( !is_null( $scale ) && !$file->isOld() ) {
+ $thumb = $file->getThumbnail( $scale['width'], $scale['height'] );
+ if( $thumb )
{
- $vals['thumburl'] = $thumb->getURL();
+ $vals['thumburl'] = wfExpandUrl( $thumb->getURL() );
$vals['thumbwidth'] = $thumb->getWidth();
$vals['thumbheight'] = $thumb->getHeight();
}
}
- $vals['url'] = $f->getURL();
+ $vals['url'] = $file->getFullURL();
+ $vals['descriptionurl'] = wfExpandUrl( $file->getDescriptionUrl() );
}
- if($this->fld_comment)
- $vals['comment'] = $f->getDescription();
- if($this->fld_sha1)
- $vals['sha1'] = wfBaseConvert($f->getSha1(), 36, 16, 40);
- if($this->fld_metadata) {
- $metadata = unserialize($f->getMetadata());
- $vals['metadata'] = $metadata ? $metadata : null;
- $this->getResult()->setIndexedTagName_recursive($vals['metadata'], 'meta');
+ if( isset( $prop['comment'] ) )
+ $vals['comment'] = $file->getDescription();
+ if( isset( $prop['sha1'] ) )
+ $vals['sha1'] = wfBaseConvert( $file->getSha1(), 36, 16, 40 );
+ if( isset( $prop['metadata'] ) ) {
+ $metadata = $file->getMetadata();
+ $vals['metadata'] = $metadata ? unserialize( $metadata ) : null;
+ $result->setIndexedTagName_recursive( $vals['metadata'], 'meta' );
}
+ if( isset( $prop['mime'] ) )
+ $vals['mime'] = $file->getMimeType();
+
+ if( isset( $prop['archivename'] ) && $file->isOld() )
+ $vals['archivename'] = $file->getArchiveName();
+
return $vals;
}
@@ -159,7 +164,9 @@ class ApiQueryImageInfo extends ApiQueryBase {
'url',
'size',
'sha1',
- 'metadata'
+ 'mime',
+ 'metadata',
+ 'archivename'
)
),
'limit' => array(
@@ -192,7 +199,8 @@ class ApiQueryImageInfo extends ApiQueryBase {
'limit' => 'How many image revisions to return',
'start' => 'Timestamp to start listing from',
'end' => 'Timestamp to stop listing at',
- 'urlwidth' => 'If iiprop=url is set, a URL to an image scaled to this width will be returned. Only the current version of the image can be scaled.',
+ 'urlwidth' => array('If iiprop=url is set, a URL to an image scaled to this width will be returned.',
+ 'Only the current version of the image can be scaled.'),
'urlheight' => 'Similar to iiurlwidth. Cannot be used without iiurlwidth',
);
}
@@ -211,6 +219,6 @@ class ApiQueryImageInfo extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryImageInfo.php 30665 2008-02-07 12:21:48Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryImageInfo.php 37504 2008-07-10 14:28:09Z catrope $';
}
}
diff --git a/includes/api/ApiQueryImages.php b/includes/api/ApiQueryImages.php
index f7405374..32c4e1b0 100644
--- a/includes/api/ApiQueryImages.php
+++ b/includes/api/ApiQueryImages.php
@@ -29,9 +29,9 @@ if (!defined('MEDIAWIKI')) {
}
/**
- * This query adds <images> subelement to all pages with the list of images embedded into those pages.
- *
- * @addtogroup API
+ * This query adds an <images> subelement to all pages with the list of images embedded into those pages.
+ *
+ * @ingroup API
*/
class ApiQueryImages extends ApiQueryGeneratorBase {
@@ -52,6 +52,7 @@ class ApiQueryImages extends ApiQueryGeneratorBase {
if ($this->getPageSet()->getGoodTitleCount() == 0)
return; // nothing to do
+ $params = $this->extractRequestParams();
$this->addFields(array (
'il_from',
'il_to'
@@ -59,16 +60,40 @@ class ApiQueryImages extends ApiQueryGeneratorBase {
$this->addTables('imagelinks');
$this->addWhereFld('il_from', array_keys($this->getPageSet()->getGoodTitles()));
- $this->addOption('ORDER BY', "il_from, il_to");
+ if(!is_null($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");
+ $ilfrom = intval($cont[0]);
+ $ilto = $this->getDb()->strencode($this->titleToKey($cont[1]));
+ $this->addWhere("il_from > $ilfrom OR ".
+ "(il_from = $ilfrom AND ".
+ "il_to >= '$ilto')");
+ }
+ # Don't order by il_from if it's constant in the WHERE clause
+ if(count($this->getPageSet()->getGoodTitles()) == 1)
+ $this->addOption('ORDER BY', 'il_to');
+ else
+ $this->addOption('ORDER BY', 'il_from, il_to');
+ $this->addOption('LIMIT', $params['limit'] + 1);
$db = $this->getDB();
$res = $this->select(__METHOD__);
if (is_null($resultPageSet)) {
-
+
$data = array();
- $lastId = 0; // database has no ID 0
+ $lastId = 0; // database has no ID 0
+ $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->il_from .
+ '|' . $this->keyToTitle($row->il_to));
+ break;
+ }
if ($lastId != $row->il_from) {
if($lastId != 0) {
$this->addPageSubItems($lastId, $data);
@@ -76,7 +101,7 @@ class ApiQueryImages extends ApiQueryGeneratorBase {
}
$lastId = $row->il_from;
}
-
+
$vals = array();
ApiQueryBase :: addTitleInfo($vals, Title :: makeTitle(NS_IMAGE, $row->il_to));
$data[] = $vals;
@@ -89,7 +114,15 @@ class ApiQueryImages extends ApiQueryGeneratorBase {
} else {
$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->il_from .
+ '|' . $this->keyToTitle($row->il_to));
+ break;
+ }
$titles[] = Title :: makeTitle(NS_IMAGE, $row->il_to);
}
$resultPageSet->populateFromTitles($titles);
@@ -98,6 +131,26 @@ class ApiQueryImages extends ApiQueryGeneratorBase {
$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 images to return',
+ 'continue' => 'When more results are available, use this to continue',
+ );
+ }
+
public function getDescription() {
return 'Returns all images contained on the given page(s)';
}
@@ -112,7 +165,6 @@ class ApiQueryImages extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryImages.php 30222 2008-01-28 19:05:26Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryImages.php 37535 2008-07-10 21:20:43Z catrope $';
}
}
-
diff --git a/includes/api/ApiQueryInfo.php b/includes/api/ApiQueryInfo.php
index 2dee22b0..9c6487b3 100644
--- a/includes/api/ApiQueryInfo.php
+++ b/includes/api/ApiQueryInfo.php
@@ -30,8 +30,8 @@ if (!defined('MEDIAWIKI')) {
/**
* A query module to show basic page information.
- *
- * @addtogroup API
+ *
+ * @ingroup API
*/
class ApiQueryInfo extends ApiQueryBase {
@@ -49,27 +49,123 @@ class ApiQueryInfo extends ApiQueryBase {
$pageSet->requestField('page_len');
}
+ protected function getTokenFunctions() {
+ // tokenname => function
+ // function prototype is func($pageid, $title)
+ // 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(
+ 'edit' => array( 'ApiQueryInfo', 'getEditToken' ),
+ 'delete' => array( 'ApiQueryInfo', 'getDeleteToken' ),
+ 'protect' => array( 'ApiQueryInfo', 'getProtectToken' ),
+ 'move' => array( 'ApiQueryInfo', 'getMoveToken' ),
+ 'block' => array( 'ApiQueryInfo', 'getBlockToken' ),
+ 'unblock' => array( 'ApiQueryInfo', 'getUnblockToken' )
+ );
+ wfRunHooks('APIQueryInfoTokens', array(&$this->tokenFunctions));
+ return $this->tokenFunctions;
+ }
+
+ public static function getEditToken($pageid, $title)
+ {
+ // We could check for $title->userCan('edit') here,
+ // but that's too expensive for this purpose
+ global $wgUser;
+ if(!$wgUser->isAllowed('edit'))
+ return false;
+
+ // The edit token is always the same, let's exploit that
+ static $cachedEditToken = null;
+ if(!is_null($cachedEditToken))
+ return $cachedEditToken;
+
+ $cachedEditToken = $wgUser->editToken();
+ return $cachedEditToken;
+ }
+
+ public static function getDeleteToken($pageid, $title)
+ {
+ global $wgUser;
+ if(!$wgUser->isAllowed('delete'))
+ return false;
+
+ static $cachedDeleteToken = null;
+ if(!is_null($cachedDeleteToken))
+ return $cachedDeleteToken;
+
+ $cachedDeleteToken = $wgUser->editToken();
+ return $cachedDeleteToken;
+ }
+
+ public static function getProtectToken($pageid, $title)
+ {
+ global $wgUser;
+ if(!$wgUser->isAllowed('protect'))
+ return false;
+
+ static $cachedProtectToken = null;
+ if(!is_null($cachedProtectToken))
+ return $cachedProtectToken;
+
+ $cachedProtectToken = $wgUser->editToken();
+ return $cachedProtectToken;
+ }
+
+ public static function getMoveToken($pageid, $title)
+ {
+ global $wgUser;
+ if(!$wgUser->isAllowed('move'))
+ return false;
+
+ static $cachedMoveToken = null;
+ if(!is_null($cachedMoveToken))
+ return $cachedMoveToken;
+
+ $cachedMoveToken = $wgUser->editToken();
+ return $cachedMoveToken;
+ }
+
+ public static function getBlockToken($pageid, $title)
+ {
+ global $wgUser;
+ if(!$wgUser->isAllowed('block'))
+ return false;
+
+ static $cachedBlockToken = null;
+ if(!is_null($cachedBlockToken))
+ return $cachedBlockToken;
+
+ $cachedBlockToken = $wgUser->editToken();
+ return $cachedBlockToken;
+ }
+
+ public static function getUnblockToken($pageid, $title)
+ {
+ // Currently, this is exactly the same as the block token
+ return self::getBlockToken($pageid, $title);
+ }
+
public function execute() {
global $wgUser;
$params = $this->extractRequestParams();
- $fld_protection = false;
+ $fld_protection = $fld_talkid = $fld_subjectid = 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']);
}
- if(!is_null($params['token'])) {
- $token = $params['token'];
- $tok_edit = $this->getTokenFlag($token, 'edit');
- $tok_delete = $this->getTokenFlag($token, 'delete');
- $tok_protect = $this->getTokenFlag($token, 'protect');
- $tok_move = $this->getTokenFlag($token, 'move');
- }
- else
- // Fix E_NOTICEs about unset variables
- $token = $tok_edit = $tok_delete = $tok_protect = $tok_move = null;
-
+
$pageSet = $this->getPageSet();
$titles = $pageSet->getGoodTitles();
$missing = $pageSet->getMissingTitles();
@@ -101,7 +197,64 @@ class ApiQueryInfo extends ApiQueryBase {
$protections[$row->pr_page][] = $a;
}
$db->freeResult($res);
+
+ $imageIds = array();
+ foreach ($titles as $id => $title)
+ if ($title->getNamespace() == NS_IMAGE)
+ $imageIds[] = $id;
+ // To avoid code duplication
+ $cascadeTypes = array(
+ array(
+ 'prefix' => 'tl',
+ 'table' => 'templatelinks',
+ 'ns' => 'tl_namespace',
+ 'title' => 'tl_title',
+ 'ids' => array_diff(array_keys($titles), $imageIds)
+ ),
+ array(
+ 'prefix' => 'il',
+ 'table' => 'imagelinks',
+ 'ns' => NS_IMAGE,
+ 'title' => 'il_to',
+ 'ids' => $imageIds
+ )
+ );
+
+ foreach ($cascadeTypes as $type)
+ {
+ if (count($type['ids']) != 0) {
+ $this->resetQueryParams();
+ $this->addTables(array('page_restrictions', $type['table']));
+ $this->addTables('page', 'page_source');
+ $this->addTables('page', 'page_target');
+ $this->addFields(array('pr_type', 'pr_level', 'pr_expiry',
+ 'page_target.page_id AS page_target_id',
+ 'page_source.page_namespace AS page_source_namespace',
+ 'page_source.page_title AS page_source_title'));
+ $this->addWhere(array("{$type['prefix']}_from = pr_page",
+ 'page_target.page_namespace = '.$type['ns'],
+ 'page_target.page_title = '.$type['title'],
+ 'page_source.page_id = pr_page'
+ ));
+ $this->addWhereFld('pr_cascade', 1);
+ $this->addWhereFld('page_target.page_id', $type['ids']);
+
+ $res = $this->select(__METHOD__);
+ while($row = $db->fetchObject($res)) {
+ $source = Title::makeTitle($row->page_source_namespace, $row->page_source_title);
+ $a = array(
+ 'type' => $row->pr_type,
+ 'level' => $row->pr_level,
+ 'expiry' => Block::decodeExpiry( $row->pr_expiry, TS_ISO_8601 ),
+ 'source' => $source->getPrefixedText()
+ );
+ $protections[$row->page_target_id][] = $a;
+ }
+ $db->freeResult($res);
+ }
+ }
}
+
// We don't need to check for pt stuff if there are no nonexistent titles
if($fld_protection && !empty($missing))
{
@@ -114,13 +267,107 @@ class ApiQueryInfo extends ApiQueryBase {
$res = $this->select(__METHOD__);
$prottitles = array();
while($row = $db->fetchObject($res)) {
- $prottitles[$row->pt_namespace][$row->pt_title] = array(
+ $prottitles[$row->pt_namespace][$row->pt_title][] = array(
'type' => 'create',
'level' => $row->pt_create_perm,
'expiry' => Block::decodeExpiry($row->pt_expiry, TS_ISO_8601)
);
}
$db->freeResult($res);
+
+ $images = array();
+ $others = array();
+ foreach ($missing as $title)
+ if ($title->getNamespace() == NS_IMAGE)
+ $images[] = $title->getDbKey();
+ else
+ $others[] = $title;
+
+ if (count($others) != 0) {
+ $lb = new LinkBatch($others);
+ $this->resetQueryParams();
+ $this->addTables(array('page_restrictions', 'page', 'templatelinks'));
+ $this->addFields(array('pr_type', 'pr_level', 'pr_expiry',
+ 'page_title', 'page_namespace',
+ 'tl_title', 'tl_namespace'));
+ $this->addWhere($lb->constructSet('tl', $db));
+ $this->addWhere('pr_page = page_id');
+ $this->addWhere('pr_page = tl_from');
+ $this->addWhereFld('pr_cascade', 1);
+
+ $res = $this->select(__METHOD__);
+ while($row = $db->fetchObject($res)) {
+ $source = Title::makeTitle($row->page_namespace, $row->page_title);
+ $a = array(
+ 'type' => $row->pr_type,
+ 'level' => $row->pr_level,
+ 'expiry' => Block::decodeExpiry( $row->pr_expiry, TS_ISO_8601 ),
+ 'source' => $source->getPrefixedText()
+ );
+ $prottitles[$row->tl_namespace][$row->tl_title][] = $a;
+ }
+ $db->freeResult($res);
+ }
+
+ if (count($images) != 0) {
+ $this->resetQueryParams();
+ $this->addTables(array('page_restrictions', 'page', 'imagelinks'));
+ $this->addFields(array('pr_type', 'pr_level', 'pr_expiry',
+ 'page_title', 'page_namespace', 'il_to'));
+ $this->addWhere('pr_page = page_id');
+ $this->addWhere('pr_page = il_from');
+ $this->addWhereFld('pr_cascade', 1);
+ $this->addWhereFld('il_to', $images);
+
+ $res = $this->select(__METHOD__);
+ while($row = $db->fetchObject($res)) {
+ $source = Title::makeTitle($row->page_namespace, $row->page_title);
+ $a = array(
+ 'type' => $row->pr_type,
+ 'level' => $row->pr_level,
+ 'expiry' => Block::decodeExpiry( $row->pr_expiry, TS_ISO_8601 ),
+ 'source' => $source->getPrefixedText()
+ );
+ $prottitles[NS_IMAGE][$row->il_to][] = $a;
+ }
+ $db->freeResult($res);
+ }
+ }
+
+ // Run the talkid/subjectid query
+ if($fld_talkid || $fld_subjectid)
+ {
+ $talktitles = $subjecttitles =
+ $talkids = $subjectids = array();
+ $everything = array_merge($titles, $missing);
+ foreach($everything as $t)
+ {
+ if(MWNamespace::isTalk($t->getNamespace()))
+ {
+ if($fld_subjectid)
+ $subjecttitles[] = $t->getSubjectPage();
+ }
+ else if($fld_talkid)
+ $talktitles[] = $t->getTalkPage();
+ }
+ if(!empty($talktitles) || !empty($subjecttitles))
+ {
+ // Construct a custom WHERE clause that matches
+ // all titles in $talktitles and $subjecttitles
+ $lb = new LinkBatch(array_merge($talktitles, $subjecttitles));
+ $this->resetQueryParams();
+ $this->addTables('page');
+ $this->addFields(array('page_title', 'page_namespace', 'page_id'));
+ $this->addWhere($lb->constructSet('page', $db));
+ $res = $this->select(__METHOD__);
+ while($row = $db->fetchObject($res))
+ {
+ if(MWNamespace::isTalk($row->page_namespace))
+ $talkids[MWNamespace::getSubject($row->page_namespace)][$row->page_title] = $row->page_id;
+ else
+ $subjectids[MWNamespace::getTalk($row->page_namespace)][$row->page_title] = $row->page_id;
+ }
+ }
}
foreach ( $titles as $pageid => $title ) {
@@ -137,18 +384,18 @@ class ApiQueryInfo extends ApiQueryBase {
if ($pageIsNew[$pageid])
$pageInfo['new'] = '';
- if (!is_null($token)) {
- // Currently all tokens are generated the same way, but it might change
- if ($tok_edit)
- $pageInfo['edittoken'] = $wgUser->editToken();
- if ($tok_delete)
- $pageInfo['deletetoken'] = $wgUser->editToken();
- if ($tok_protect)
- $pageInfo['protecttoken'] = $wgUser->editToken();
- if ($tok_move)
- $pageInfo['movetoken'] = $wgUser->editToken();
+ if (!is_null($params['token'])) {
+ $tokenFunctions = $this->getTokenFunctions();
+ foreach($params['token'] as $t)
+ {
+ $val = call_user_func($tokenFunctions[$t], $pageid, $title);
+ if($val === false)
+ $this->setWarning("Action '$t' is not allowed for the current user");
+ else
+ $pageInfo[$t . 'token'] = $val;
+ }
}
-
+
if($fld_protection) {
if (isset($protections[$pageid])) {
$pageInfo['protection'] = $protections[$pageid];
@@ -186,6 +433,10 @@ class ApiQueryInfo extends ApiQueryBase {
}
}
}
+ 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()];
$result->addValue(array (
'query',
@@ -195,24 +446,34 @@ class ApiQueryInfo extends ApiQueryBase {
// Get edit/protect tokens and protection data for missing titles if requested
// Delete and move tokens are N/A for missing titles anyway
- if($tok_edit || $tok_protect || $fld_protection)
+ if(!is_null($params['token']) || $fld_protection || $fld_talkid || $fld_subjectid)
{
$res = &$result->getData();
foreach($missing as $pageid => $title) {
- if($tok_edit)
- $res['query']['pages'][$pageid]['edittoken'] = $wgUser->editToken();
- if($tok_protect)
- $res['query']['pages'][$pageid]['protecttoken'] = $wgUser->editToken();
+ if(!is_null($params['token']))
+ {
+ $tokenFunctions = $this->getTokenFunctions();
+ foreach($params['token'] as $t)
+ {
+ $val = call_user_func($tokenFunctions[$t], $pageid, $title);
+ if($val !== false)
+ $res['query']['pages'][$pageid][$t . 'token'] = $val;
+ }
+ }
if($fld_protection)
{
// Apparently the XML formatting code doesn't like array(null)
// This is painful to fix, so we'll just work around it
if(isset($prottitles[$title->getNamespace()][$title->getDBkey()]))
- $res['query']['pages'][$pageid]['protection'][] = $prottitles[$title->getNamespace()][$title->getDBkey()];
+ $res['query']['pages'][$pageid]['protection'] = $prottitles[$title->getNamespace()][$title->getDBkey()];
else
$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()];
}
}
}
@@ -223,17 +484,15 @@ class ApiQueryInfo extends ApiQueryBase {
ApiBase :: PARAM_DFLT => NULL,
ApiBase :: PARAM_ISMULTI => true,
ApiBase :: PARAM_TYPE => array (
- 'protection'
+ 'protection',
+ 'talkid',
+ 'subjectid'
)),
'token' => array (
ApiBase :: PARAM_DFLT => NULL,
ApiBase :: PARAM_ISMULTI => true,
- ApiBase :: PARAM_TYPE => array (
- 'edit',
- 'delete',
- 'protect',
- 'move',
- )),
+ ApiBase :: PARAM_TYPE => array_keys($this->getTokenFunctions())
+ )
);
}
@@ -241,7 +500,9 @@ class ApiQueryInfo extends ApiQueryBase {
return array (
'prop' => array (
'Which additional properties to get:',
- ' "protection" - List the protection level of each page'
+ ' "protection" - List the protection level of each page',
+ ' "talkid" - The page ID of the talk page for each non-talk page',
+ ' "subjectid" - The page ID of the parent page for each talk page'
),
'token' => 'Request a token to perform a data-modifying action on a page',
);
@@ -260,7 +521,6 @@ class ApiQueryInfo extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryInfo.php 30222 2008-01-28 19:05:26Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryInfo.php 37191 2008-07-06 18:43:06Z brion $';
}
}
-
diff --git a/includes/api/ApiQueryLangLinks.php b/includes/api/ApiQueryLangLinks.php
index 04a930db..e7d84fc3 100644
--- a/includes/api/ApiQueryLangLinks.php
+++ b/includes/api/ApiQueryLangLinks.php
@@ -30,8 +30,8 @@ if (!defined('MEDIAWIKI')) {
/**
* A query module to list all langlinks (links to correspanding foreign language pages).
- *
- * @addtogroup API
+ *
+ * @ingroup API
*/
class ApiQueryLangLinks extends ApiQueryBase {
@@ -40,6 +40,10 @@ class ApiQueryLangLinks extends ApiQueryBase {
}
public function execute() {
+ if ( $this->getPageSet()->getGoodTitleCount() == 0 )
+ return;
+
+ $params = $this->extractRequestParams();
$this->addFields(array (
'll_from',
'll_lang',
@@ -48,14 +52,36 @@ class ApiQueryLangLinks extends ApiQueryBase {
$this->addTables('langlinks');
$this->addWhereFld('ll_from', array_keys($this->getPageSet()->getGoodTitles()));
- $this->addOption('ORDER BY', "ll_from, ll_lang");
+ if(!is_null($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");
+ $llfrom = intval($cont[0]);
+ $lllang = $this->getDb()->strencode($cont[1]);
+ $this->addWhere("ll_from > $llfrom OR ".
+ "(ll_from = $llfrom AND ".
+ "ll_lang >= '$lllang')");
+ }
+ # Don't order by ll_from if it's constant in the WHERE clause
+ if(count($this->getPageSet()->getGoodTitles()) == 1)
+ $this->addOption('ORDER BY', 'll_lang');
+ else
+ $this->addOption('ORDER BY', 'll_from, ll_lang');
+ $this->addOption('LIMIT', $params['limit'] + 1);
$res = $this->select(__METHOD__);
$data = array();
- $lastId = 0; // database has no ID 0
+ $lastId = 0; // database has no ID 0
+ $count = 0;
$db = $this->getDB();
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->ll_from}|{$row->ll_lang}");
+ break;
+ }
if ($lastId != $row->ll_from) {
if($lastId != 0) {
$this->addPageSubItems($lastId, $data);
@@ -64,7 +90,7 @@ class ApiQueryLangLinks extends ApiQueryBase {
$lastId = $row->ll_from;
}
- $entry = array('lang'=>$row->ll_lang);
+ $entry = array('lang' => $row->ll_lang);
ApiResult :: setContent($entry, $row->ll_title);
$data[] = $entry;
}
@@ -76,6 +102,26 @@ class ApiQueryLangLinks extends ApiQueryBase {
$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 langlinks to return',
+ 'continue' => 'When more results are available, use this to continue',
+ );
+ }
+
public function getDescription() {
return 'Returns all interlanguage links from the given page(s)';
}
@@ -88,7 +134,6 @@ class ApiQueryLangLinks extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryLangLinks.php 30222 2008-01-28 19:05:26Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryLangLinks.php 37534 2008-07-10 21:08:37Z brion $';
}
}
-
diff --git a/includes/api/ApiQueryLinks.php b/includes/api/ApiQueryLinks.php
index d77e627a..546a599d 100644
--- a/includes/api/ApiQueryLinks.php
+++ b/includes/api/ApiQueryLinks.php
@@ -30,8 +30,8 @@ if (!defined('MEDIAWIKI')) {
/**
* A query module to list all wiki links on a given set of pages.
- *
- * @addtogroup API
+ *
+ * @ingroup API
*/
class ApiQueryLinks extends ApiQueryGeneratorBase {
@@ -41,7 +41,7 @@ class ApiQueryLinks extends ApiQueryGeneratorBase {
private $table, $prefix, $description;
public function __construct($query, $moduleName) {
-
+
switch ($moduleName) {
case self::LINKS :
$this->table = 'pagelinks';
@@ -84,16 +84,54 @@ class ApiQueryLinks extends ApiQueryGeneratorBase {
$this->addTables($this->table);
$this->addWhereFld($this->prefix . '_from', array_keys($this->getPageSet()->getGoodTitles()));
$this->addWhereFld($this->prefix . '_namespace', $params['namespace']);
- $this->addOption('ORDER BY', str_replace('pl_', $this->prefix . '_', 'pl_from, pl_namespace, pl_title'));
+
+ if(!is_null($params['continue'])) {
+ $cont = explode('|', $params['continue']);
+ if(count($cont) != 3)
+ $this->dieUsage("Invalid continue param. You should pass the " .
+ "original value returned by the previous query", "_badcontinue");
+ $plfrom = intval($cont[0]);
+ $plns = intval($cont[1]);
+ $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 ".
+ "({$this->prefix}_namespace = $plns AND ".
+ "{$this->prefix}_title >= '$pltitle')))");
+ }
+
+ # Here's some MySQL craziness going on: if you use WHERE foo='bar'
+ # and later ORDER BY foo MySQL doesn't notice the ORDER BY is pointless
+ # but instead goes and filesorts, because the index for foo was used
+ # already. To work around this, we drop constant fields in the WHERE
+ # clause from the ORDER BY clause
+ $order = array();
+ if(count($this->getPageSet()->getGoodTitles()) != 1)
+ $order[] = "{$this->prefix}_from";
+ if(count($params['namespace']) != 1)
+ $order[] = "{$this->prefix}_namespace";
+ $order[] = "{$this->prefix}_title";
+ $this->addOption('ORDER BY', implode(", ", $order));
+ $this->addOption('USE INDEX', "{$this->prefix}_from");
+ $this->addOption('LIMIT', $params['limit'] + 1);
$db = $this->getDB();
$res = $this->select(__METHOD__);
if (is_null($resultPageSet)) {
-
+
$data = array();
- $lastId = 0; // database has no ID 0
+ $lastId = 0; // database has no ID 0
+ $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->pl_from}|{$row->pl_namespace}|" .
+ $this->keyToTitle($row->pl_title));
+ break;
+ }
if ($lastId != $row->pl_from) {
if($lastId != 0) {
$this->addPageSubItems($lastId, $data);
@@ -114,7 +152,16 @@ class ApiQueryLinks extends ApiQueryGeneratorBase {
} else {
$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->pl_from}|{$row->pl_namespace}|" .
+ $this->keyToTitle($row->pl_title));
+ break;
+ }
$titles[] = Title :: makeTitle($row->pl_namespace, $row->pl_title);
}
$resultPageSet->populateFromTitles($titles);
@@ -129,15 +176,25 @@ class ApiQueryLinks extends ApiQueryGeneratorBase {
'namespace' => array(
ApiBase :: PARAM_TYPE => 'namespace',
ApiBase :: PARAM_ISMULTI => true
- )
+ ),
+ '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(
- 'namespace' => "Show {$this->description}s in this namespace(s) only"
- );
+ 'namespace' => "Show {$this->description}s in this namespace(s) only",
+ 'limit' => "How many {$this->description}s to return",
+ 'continue' => 'When more results are available, use this to continue',
+ );
}
public function getDescription() {
@@ -156,7 +213,6 @@ class ApiQueryLinks extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryLinks.php 30222 2008-01-28 19:05:26Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryLinks.php 37909 2008-07-22 13:26:15Z catrope $';
}
}
-
diff --git a/includes/api/ApiQueryLogEvents.php b/includes/api/ApiQueryLogEvents.php
index e25e5275..47a526bb 100644
--- a/includes/api/ApiQueryLogEvents.php
+++ b/includes/api/ApiQueryLogEvents.php
@@ -30,8 +30,8 @@ if (!defined('MEDIAWIKI')) {
/**
* Query action to List the log events, with optional filtering by various parameters.
- *
- * @addtogroup API
+ *
+ * @ingroup API
*/
class ApiQueryLogEvents extends ApiQueryBase {
@@ -40,7 +40,7 @@ class ApiQueryLogEvents extends ApiQueryBase {
}
public function execute() {
- $params = $this->extractRequestParams();
+ $params = $this->extractRequestParams();
$db = $this->getDB();
$prop = $params['prop'];
@@ -54,19 +54,26 @@ class ApiQueryLogEvents extends ApiQueryBase {
list($tbl_logging, $tbl_page, $tbl_user) = $db->tableNamesN('logging', 'page', 'user');
- $this->addOption('STRAIGHT_JOIN');
- $this->addTables("$tbl_logging LEFT OUTER JOIN $tbl_page ON " .
- "log_namespace=page_namespace AND log_title=page_title " .
- "INNER JOIN $tbl_user ON user_id=log_user");
+ $hideLogs = LogEventsList::getExcludeClause($db);
+ if($hideLogs !== false)
+ $this->addWhere($hideLogs);
+
+ // Order is significant here
+ $this->addTables(array('user', 'page', 'logging'));
+ $this->addJoinConds(array(
+ 'page' => array('LEFT JOIN',
+ array( 'log_namespace=page_namespace',
+ 'log_title=page_title'))));
+ $this->addWhere('user_id=log_user');
+ $this->addOption('USE INDEX', array('logging' => 'times')); // default, may change
$this->addFields(array (
'log_type',
'log_action',
'log_timestamp',
));
-
- // FIXME: Fake out log_id for now until the column is live on Wikimedia
- // $this->addFieldsIf('log_id', $this->fld_ids);
+
+ $this->addFieldsIf('log_id', $this->fld_ids);
$this->addFieldsIf('page_id', $this->fld_ids);
$this->addFieldsIf('log_user', $this->fld_user);
$this->addFieldsIf('user_name', $this->fld_user);
@@ -74,10 +81,14 @@ class ApiQueryLogEvents extends ApiQueryBase {
$this->addFieldsIf('log_title', $this->fld_title);
$this->addFieldsIf('log_comment', $this->fld_comment);
$this->addFieldsIf('log_params', $this->fld_details);
-
$this->addWhereFld('log_deleted', 0);
- $this->addWhereFld('log_type', $params['type']);
+
+ if( !is_null($params['type']) ) {
+ $this->addWhereFld('log_type', $params['type']);
+ $this->addOption('USE INDEX', array('logging' => array('type_time')));
+ }
+
$this->addWhereRange('log_timestamp', $params['dir'], $params['start'], $params['end']);
$limit = $params['limit'];
@@ -91,6 +102,7 @@ class ApiQueryLogEvents extends ApiQueryBase {
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')));
}
$title = $params['title'];
@@ -100,6 +112,7 @@ 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')));
}
$data = array ();
@@ -126,26 +139,24 @@ class ApiQueryLogEvents extends ApiQueryBase {
$vals = array();
if ($this->fld_ids) {
- // FIXME: Fake out log_id for now until the column is live on Wikimedia
- // $vals['logid'] = intval($row->log_id);
- $vals['logid'] = 0;
+ $vals['logid'] = intval($row->log_id);
$vals['pageid'] = intval($row->page_id);
}
-
+
if ($this->fld_title) {
$title = Title :: makeTitle($row->log_namespace, $row->log_title);
ApiQueryBase :: addTitleInfo($vals, $title);
}
-
+
if ($this->fld_type) {
$vals['type'] = $row->log_type;
$vals['action'] = $row->log_action;
}
-
+
if ($this->fld_details && $row->log_params !== '') {
$params = explode("\n", $row->log_params);
switch ($row->log_type) {
- case 'move':
+ case 'move':
if (isset ($params[0])) {
$title = Title :: newFromText($params[0]);
if ($title) {
@@ -175,7 +186,7 @@ class ApiQueryLogEvents extends ApiQueryBase {
$params = null;
break;
}
-
+
if (isset($params)) {
$this->getResult()->setIndexedTagName($params, 'param');
$vals = array_merge($vals, $params);
@@ -193,7 +204,7 @@ class ApiQueryLogEvents extends ApiQueryBase {
if ($this->fld_comment && !empty ($row->log_comment)) {
$vals['comment'] = $row->log_comment;
}
-
+
return $vals;
}
@@ -215,7 +226,6 @@ class ApiQueryLogEvents extends ApiQueryBase {
)
),
'type' => array (
- ApiBase :: PARAM_ISMULTI => true,
ApiBase :: PARAM_TYPE => $wgLogTypes
),
'start' => array (
@@ -267,7 +277,6 @@ class ApiQueryLogEvents extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryLogEvents.php 30222 2008-01-28 19:05:26Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryLogEvents.php 35098 2008-05-20 17:13:28Z ialex $';
}
}
-
diff --git a/includes/api/ApiQueryRandom.php b/includes/api/ApiQueryRandom.php
index b8282098..046157a6 100644
--- a/includes/api/ApiQueryRandom.php
+++ b/includes/api/ApiQueryRandom.php
@@ -30,24 +30,24 @@ if (!defined('MEDIAWIKI')) {
/**
* Query module to get list of random pages
- *
- * @addtogroup API
+ *
+ * @ingroup API
*/
-
+
class ApiQueryRandom extends ApiQueryGeneratorBase {
public function __construct($query, $moduleName) {
parent :: __construct($query, $moduleName, 'rn');
}
-
+
public function execute() {
$this->run();
}
-
+
public function executeGenerator($resultPageSet) {
$this->run($resultPageSet);
}
-
+
protected function prepareQuery($randstr, $limit, $namespace, &$resultPageSet) {
$this->resetQueryParams();
$this->addTables('page');
@@ -104,7 +104,7 @@ if (!defined('MEDIAWIKI')) {
if(is_null($resultPageSet)) {
$result->setIndexedTagName($data, 'page');
$result->addValue('query', $this->getModuleName(), $data);
- }
+ }
}
private function extractRowInfo($row) {
@@ -115,7 +115,7 @@ if (!defined('MEDIAWIKI')) {
$vals['id'] = $row->page_id;
return $vals;
}
-
+
public function getAllowedParams() {
return array (
'namespace' => array(
diff --git a/includes/api/ApiQueryRecentChanges.php b/includes/api/ApiQueryRecentChanges.php
index 44093854..2b8c6a92 100644
--- a/includes/api/ApiQueryRecentChanges.php
+++ b/includes/api/ApiQueryRecentChanges.php
@@ -31,8 +31,8 @@ if (!defined('MEDIAWIKI')) {
/**
* A query action to enumerate the recent changes that were done to the wiki.
* Various filters are supported.
- *
- * @addtogroup API
+ *
+ * @ingroup API
*/
class ApiQueryRecentChanges extends ApiQueryBase {
@@ -43,26 +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;
-
+
/**
* Generates and outputs the result of this query based upon the provided parameters.
*/
public function execute() {
/* Initialize vars */
- $limit = $prop = $namespace = $show = $type = $dir = $start = $end = null;
-
+ $limit = $prop = $namespace = $titles = $show = $type = $dir = $start = $end = null;
+
/* Get the parameters of the request. */
extract($this->extractRequestParams());
/* Build our basic query. Namely, something along the lines of:
- * SELECT * from recentchanges WHERE rc_timestamp > $start
- * AND rc_timestamp < $end AND rc_namespace = $namespace
+ * SELECT * FROM recentchanges WHERE rc_timestamp > $start
+ * AND rc_timestamp < $end AND rc_namespace = $namespace
* AND rc_deleted = '0'
*/
+ $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->addWhereFld('rc_deleted', 0);
+ if(!empty($titles))
+ {
+ $lb = new LinkBatch;
+ foreach($titles as $t)
+ {
+ $obj = Title::newFromText($t);
+ $lb->addObj($obj);
+ if($obj->getNamespace() < 0)
+ {
+ // 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;
+ }
+ }
+ $where = $lb->constructSet('rc', $this->getDb());
+ if($where != '')
+ $this->addWhere($where);
+ }
+
if(!is_null($type))
$this->addWhereFld('rc_type', $this->parseRCType($type));
@@ -70,12 +92,19 @@ class ApiQueryRecentChanges extends ApiQueryBase {
$show = array_flip($show);
/* Check for conflicting parameters. */
- if ((isset ($show['minor']) && isset ($show['!minor']))
- || (isset ($show['bot']) && isset ($show['!bot']))
- || (isset ($show['anon']) && isset ($show['!anon']))) {
-
+ if ((isset ($show['minor']) && isset ($show['!minor']))
+ || (isset ($show['bot']) && isset ($show['!bot']))
+ || (isset ($show['anon']) && isset ($show['!anon']))
+ || (isset ($show['redirect']) && isset ($show['!redirect']))
+ || (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->isAllowed('patrol'))
+ $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']));
@@ -84,6 +113,11 @@ class ApiQueryRecentChanges extends ApiQueryBase {
$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->addWhereIf('page_is_redirect = 1', isset ($show['redirect']));
+ // Don't throw log entries out the window here
+ $this->addWhereIf('page_is_redirect = 0 OR page_is_redirect IS NULL', isset ($show['!redirect']));
}
/* Add the fields we're concerned with to out query. */
@@ -108,13 +142,19 @@ class ApiQueryRecentChanges extends ApiQueryBase {
$this->fld_title = isset ($prop['title']);
$this->fld_ids = isset ($prop['ids']);
$this->fld_sizes = isset ($prop['sizes']);
+ $this->fld_redirect = isset($prop['redirect']);
+ $this->fld_patrolled = isset($prop['patrolled']);
+
+ global $wgUser;
+ if($this->fld_patrolled && !$wgUser->isAllowed('patrol'))
+ $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);
+ $this->addFieldsIf('rc_id', $this->fld_ids);
+ $this->addFieldsIf('rc_cur_id', $this->fld_ids);
+ $this->addFieldsIf('rc_this_oldid', $this->fld_ids);
+ $this->addFieldsIf('rc_last_oldid', $this->fld_ids);
+ $this->addFieldsIf('rc_comment', $this->fld_comment);
$this->addFieldsIf('rc_user', $this->fld_user);
$this->addFieldsIf('rc_user_text', $this->fld_user);
$this->addFieldsIf('rc_minor', $this->fld_flags);
@@ -122,15 +162,18 @@ class ApiQueryRecentChanges extends ApiQueryBase {
$this->addFieldsIf('rc_new', $this->fld_flags);
$this->addFieldsIf('rc_old_len', $this->fld_sizes);
$this->addFieldsIf('rc_new_len', $this->fld_sizes);
+ $this->addFieldsIf('rc_patrolled', $this->fld_patrolled);
+ if($this->fld_redirect || isset($show['redirect']) || isset($show['!redirect']))
+ {
+ $this->addTables('page');
+ $this->addJoinConds(array('page' => array('LEFT JOIN', array('rc_namespace=page_namespace', 'rc_title=page_title'))));
+ $this->addFields('page_is_redirect');
+ }
}
-
- /* Specify the limit for our query. It's $limit+1 because we (possibly) need to
+ /* 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);
- /* Specify the index to use in the query as rc_timestamp, instead of rc_revid (default). */
- $this->addOption('USE INDEX', 'rc_timestamp');
-
$data = array ();
$count = 0;
@@ -148,7 +191,7 @@ class ApiQueryRecentChanges extends ApiQueryBase {
/* Extract the data from a single row. */
$vals = $this->extractRowInfo($row);
-
+
/* Add that row's data to our final output. */
if($vals)
$data[] = $vals;
@@ -240,9 +283,17 @@ class ApiQueryRecentChanges extends ApiQueryBase {
$vals['comment'] = $row->rc_comment;
}
+ if ($this->fld_redirect)
+ if($row->page_is_redirect)
+ $vals['redirect'] = '';
+
+ /* Add the patrolled flag */
+ if ($this->fld_patrolled && $row->rc_patrolled == 1)
+ $vals['patrolled'] = '';
+
return $vals;
}
-
+
private function parseRCType($type)
{
if(is_array($type))
@@ -258,7 +309,7 @@ class ApiQueryRecentChanges extends ApiQueryBase {
case 'new': return RC_NEW;
case 'log': return RC_LOG;
}
- }
+ }
public function getAllowedParams() {
return array (
@@ -279,6 +330,9 @@ class ApiQueryRecentChanges extends ApiQueryBase {
ApiBase :: PARAM_ISMULTI => true,
ApiBase :: PARAM_TYPE => 'namespace'
),
+ 'titles' => array(
+ ApiBase :: PARAM_ISMULTI => true
+ ),
'prop' => array (
ApiBase :: PARAM_ISMULTI => true,
ApiBase :: PARAM_DFLT => 'title|timestamp|ids',
@@ -289,7 +343,9 @@ class ApiQueryRecentChanges extends ApiQueryBase {
'timestamp',
'title',
'ids',
- 'sizes'
+ 'sizes',
+ 'redirect',
+ 'patrolled'
)
),
'show' => array (
@@ -300,7 +356,11 @@ class ApiQueryRecentChanges extends ApiQueryBase {
'bot',
'!bot',
'anon',
- '!anon'
+ '!anon',
+ 'redirect',
+ '!redirect',
+ 'patrolled',
+ '!patrolled'
)
),
'limit' => array (
@@ -314,7 +374,7 @@ class ApiQueryRecentChanges extends ApiQueryBase {
ApiBase :: PARAM_ISMULTI => true,
ApiBase :: PARAM_TYPE => array (
'edit',
- 'new',
+ 'new',
'log'
)
)
@@ -327,13 +387,14 @@ class ApiQueryRecentChanges extends ApiQueryBase {
'end' => 'The timestamp to end enumerating.',
'dir' => 'In which direction to enumerate.',
'namespace' => 'Filter log entries to only this namespace(s)',
+ 'titles' => 'Filter log entries to only these page titles',
'prop' => 'Include additional pieces of information',
'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'
),
'type' => 'Which types of changes to show.',
- 'limit' => 'How many total pages to return.'
+ 'limit' => 'How many total changes to return.'
);
}
@@ -348,7 +409,6 @@ class ApiQueryRecentChanges extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryRecentChanges.php 30222 2008-01-28 19:05:26Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryRecentChanges.php 37909 2008-07-22 13:26:15Z catrope $';
}
}
-
diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php
index e22d3b30..1fd2d7c6 100644
--- a/includes/api/ApiQueryRevisions.php
+++ b/includes/api/ApiQueryRevisions.php
@@ -30,10 +30,10 @@ if (!defined('MEDIAWIKI')) {
/**
* A query action to enumerate revisions of a given page, or show top revisions of multiple pages.
- * Various pieces of information may be shown - flags, comments, and the actual wiki markup of the rev.
- * In the enumeration mode, ranges of revisions may be requested and filtered.
- *
- * @addtogroup API
+ * Various pieces of information may be shown - flags, comments, and the actual wiki markup of the rev.
+ * In the enumeration mode, ranges of revisions may be requested and filtered.
+ *
+ * @ingroup API
*/
class ApiQueryRevisions extends ApiQueryBase {
@@ -44,16 +44,45 @@ class ApiQueryRevisions extends ApiQueryBase {
private $fld_ids = false, $fld_flags = false, $fld_timestamp = false, $fld_size = false,
$fld_comment = false, $fld_user = false, $fld_content = 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(
+ 'rollback' => array( 'ApiQueryRevisions','getRollbackToken' )
+ );
+ wfRunHooks('APIQueryRevisionsTokens', array(&$this->tokenFunctions));
+ return $this->tokenFunctions;
+ }
+
+ public static function getRollbackToken($pageid, $title, $rev)
+ {
+ global $wgUser;
+ if(!$wgUser->isAllowed('rollback'))
+ return false;
+ return $wgUser->editToken(array($title->getPrefixedText(),
+ $rev->getUserText()));
+ }
+
public function execute() {
- $limit = $startid = $endid = $start = $end = $dir = $prop = $user = $excludeuser = $token = null;
+ $limit = $startid = $endid = $start = $end = $dir = $prop = $user = $excludeuser = $expandtemplates = $section = $token = null;
extract($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
+ // 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));
-
+
$pageSet = $this->getPageSet();
$pageCount = $pageSet->getGoodTitleCount();
@@ -70,35 +99,26 @@ class ApiQueryRevisions extends ApiQueryBase {
$this->dieUsage('titles, pageids or a generator was used to supply multiple pages, but the limit, startid, endid, dirNewer, user, excludeuser, start and end parameters may only be used on a single page.', 'multpages');
$this->addTables('revision');
- $this->addWhere('rev_deleted=0');
+ $this->addFields( Revision::selectFields() );
$prop = array_flip($prop);
- // These field are needed regardless of the client requesting them
- $this->addFields('rev_id');
- $this->addFields('rev_page');
-
// Optional fields
$this->fld_ids = isset ($prop['ids']);
// $this->addFieldsIf('rev_text_id', $this->fld_ids); // should this be exposed?
- $this->fld_flags = $this->addFieldsIf('rev_minor_edit', isset ($prop['flags']));
- $this->fld_timestamp = $this->addFieldsIf('rev_timestamp', isset ($prop['timestamp']));
- $this->fld_comment = $this->addFieldsIf('rev_comment', isset ($prop['comment']));
- $this->fld_size = $this->addFieldsIf('rev_len', isset ($prop['size']));
- $this->tok_rollback = false; // Prevent PHP undefined property notice
- if(!is_null($token))
- {
- $this->tok_rollback = $this->getTokenFlag($token, 'rollback');
+ $this->fld_flags = isset ($prop['flags']);
+ $this->fld_timestamp = isset ($prop['timestamp']);
+ $this->fld_comment = isset ($prop['comment']);
+ $this->fld_size = isset ($prop['size']);
+ $this->fld_user = isset ($prop['user']);
+ $this->token = $token;
+
+ if ( !is_null($this->token) || ( $this->fld_content && $this->expandTemplates ) || $pageCount > 0) {
+ $this->addTables( 'page' );
+ $this->addWhere('page_id=rev_page');
+ $this->addFields( Revision::selectPageFields() );
}
- if (isset ($prop['user'])) {
- $this->addFields('rev_user');
- $this->addFields('rev_user_text');
- $this->fld_user = true;
- }
- else if($this->tok_rollback)
- $this->addFields('rev_user_text');
-
if (isset ($prop['content'])) {
// For each page we will request, the user must have read rights for that page
@@ -112,12 +132,15 @@ class ApiQueryRevisions extends ApiQueryBase {
$this->addTables('text');
$this->addWhere('rev_text_id=old_id');
$this->addFields('old_id');
- $this->addFields('old_text');
- $this->addFields('old_flags');
+ $this->addFields( Revision::selectTextFields() );
$this->fld_content = true;
-
+
$this->expandTemplates = $expandtemplates;
+ if(isset($section))
+ $this->section = $section;
+ else
+ $this->section = false;
}
$userMax = ( $this->fld_content ? ApiBase::LIMIT_SML1 : ApiBase::LIMIT_BIG1 );
@@ -137,13 +160,13 @@ class ApiQueryRevisions extends ApiQueryBase {
$this->dieUsage('end and endid cannot be used together', 'badparams');
if(!is_null($user) && !is_null( $excludeuser))
- $this->dieUsage('user and excludeuser cannot be used together', 'badparams');
-
+ $this->dieUsage('user and excludeuser cannot be used together', 'badparams');
+
// This code makes an assumption that sorting by rev_id and rev_timestamp produces
// the same result. This way users may request revisions starting at a given time,
// but to page through results use the rev_id returned after each page.
- // Switching to rev_id removes the potential problem of having more than
- // one row with the same timestamp for the same page.
+ // Switching to rev_id removes the potential problem of having more than
+ // one row with the same timestamp for the same page.
// The order needs to be the same as start parameter to avoid SQL filesort.
if (is_null($startid) && is_null($endid))
@@ -158,7 +181,7 @@ 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)) {
@@ -177,7 +200,6 @@ class ApiQueryRevisions extends ApiQueryBase {
elseif ($pageCount > 0) {
// When working in multi-page non-enumeration mode,
// limit to the latest revision only
- $this->addTables('page');
$this->addWhere('page_id=rev_page');
$this->addWhere('page_latest=rev_id');
$this->validateLimit('page_count', $pageCount, 1, $userMax, $botMax);
@@ -207,17 +229,18 @@ class ApiQueryRevisions extends ApiQueryBase {
break;
}
+ $revision = new Revision( $row );
$this->getResult()->addValue(
array (
'query',
'pages',
- intval($row->rev_page),
+ $revision->getPage(),
'revisions'),
null,
- $this->extractRowInfo($row));
+ $this->extractRowInfo( $revision ));
}
$db->freeResult($res);
-
+
// Ensure that all revisions are shown as '<rev>' elements
$result = $this->getResult();
if ($result->getIsRawMode()) {
@@ -230,53 +253,70 @@ class ApiQueryRevisions extends ApiQueryBase {
}
}
- private function extractRowInfo($row) {
+ private function extractRowInfo( $revision ) {
$vals = array ();
if ($this->fld_ids) {
- $vals['revid'] = intval($row->rev_id);
+ $vals['revid'] = $revision->getId();
// $vals['oldid'] = intval($row->rev_text_id); // todo: should this be exposed?
}
-
- if ($this->fld_flags && $row->rev_minor_edit)
+
+ if ($this->fld_flags && $revision->isMinor())
$vals['minor'] = '';
if ($this->fld_user) {
- $vals['user'] = $row->rev_user_text;
- if (!$row->rev_user)
+ $vals['user'] = $revision->getUserText();
+ if (!$revision->getUser())
$vals['anon'] = '';
}
if ($this->fld_timestamp) {
- $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->rev_timestamp);
+ $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $revision->getTimestamp());
}
-
- if ($this->fld_size && !is_null($row->rev_len)) {
- $vals['size'] = intval($row->rev_len);
+
+ if ($this->fld_size && !is_null($revision->getSize())) {
+ $vals['size'] = $revision->getSize();
}
- if ($this->fld_comment && !empty ($row->rev_comment)) {
- $vals['comment'] = $row->rev_comment;
+ if ($this->fld_comment) {
+ $comment = $revision->getComment();
+ if (!empty($comment))
+ $vals['comment'] = $comment;
}
-
- if($this->tok_rollback || ($this->fld_content && $this->expandTemplates))
- $title = Title::newFromID($row->rev_page);
-
- if($this->tok_rollback) {
- global $wgUser;
- $vals['rollbacktoken'] = $wgUser->editToken(array($title->getPrefixedText(), $row->rev_user_text));
+
+ if(!is_null($this->token) || ($this->fld_content && $this->expandTemplates))
+ $title = $revision->getTitle();
+
+ if(!is_null($this->token))
+ {
+ $tokenFunctions = $this->getTokenFunctions();
+ foreach($this->token as $t)
+ {
+ $val = call_user_func($tokenFunctions[$t], $title->getArticleID(), $title, $revision);
+ if($val === false)
+ $this->setWarning("Action '$t' is not allowed for the current user");
+ else
+ $vals[$t . 'token'] = $val;
+ }
}
-
-
+
if ($this->fld_content) {
- $text = Revision :: getRevisionText($row);
+ global $wgParser;
+ $text = $revision->getText();
+ # Expand templates after getting section content because
+ # template-added sections don't count and Parser::preprocess()
+ # will have less input
+ if ($this->section !== false) {
+ $text = $wgParser->getSection( $text, $this->section, false);
+ if($text === false)
+ $this->dieUsage("There is no section {$this->section} in r".$revision->getId(), 'nosuchsection');
+ }
if ($this->expandTemplates) {
- global $wgParser;
$text = $wgParser->preprocess( $text, $title, new ParserOptions() );
}
ApiResult :: setContent($vals, $text);
- }
+ }
return $vals;
}
@@ -326,12 +366,13 @@ class ApiQueryRevisions extends ApiQueryBase {
'excludeuser' => array(
ApiBase :: PARAM_TYPE => 'user'
),
-
+
'expandtemplates' => false,
+ 'section' => array(
+ ApiBase :: PARAM_TYPE => 'integer'
+ ),
'token' => array(
- ApiBase :: PARAM_TYPE => array(
- 'rollback'
- ),
+ ApiBase :: PARAM_TYPE => array_keys($this->getTokenFunctions()),
ApiBase :: PARAM_ISMULTI => true
),
);
@@ -349,6 +390,7 @@ class ApiQueryRevisions extends ApiQueryBase {
'user' => 'only include revisions made by user',
'excludeuser' => 'exclude revisions made by user',
'expandtemplates' => 'expand templates in revision content',
+ 'section' => 'only retrieve the content of this section',
'token' => 'Which tokens to obtain for each revision',
);
}
@@ -382,7 +424,6 @@ class ApiQueryRevisions extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryRevisions.php 31259 2008-02-25 14:14:55Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryRevisions.php 37300 2008-07-08 08:42:27Z btongminh $';
}
}
-
diff --git a/includes/api/ApiQuerySearch.php b/includes/api/ApiQuerySearch.php
index b15f36ce..84a2ec63 100644
--- a/includes/api/ApiQuerySearch.php
+++ b/includes/api/ApiQuerySearch.php
@@ -30,8 +30,8 @@ if (!defined('MEDIAWIKI')) {
/**
* Query module to perform full text search within wiki titles and content
- *
- * @addtogroup API
+ *
+ * @ingroup API
*/
class ApiQuerySearch extends ApiQueryGeneratorBase {
@@ -52,7 +52,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
$params = $this->extractRequestParams();
$limit = $params['limit'];
- $query = $params['search'];
+ $query = $params['search'];
if (is_null($query) || empty($query))
$this->dieUsage("empty search string is not allowed", 'param-search');
@@ -60,11 +60,14 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
$search->setLimitOffset( $limit+1, $params['offset'] );
$search->setNamespaces( $params['namespace'] );
$search->showRedirects = $params['redirects'];
-
+
if ($params['what'] == 'text')
$matches = $search->searchText( $query );
else
$matches = $search->searchTitle( $query );
+ if (is_null($matches))
+ $this->dieUsage("{$params['what']} search is disabled",
+ "search-{$params['what']}-disabled");
$data = array ();
$count = 0;
@@ -75,6 +78,9 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
break;
}
+ // Silently skip broken titles
+ if ($result->isBrokenTitle()) continue;
+
$title = $result->getTitle();
if (is_null($resultPageSet)) {
$data[] = array(
@@ -100,7 +106,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
'namespace' => array (
ApiBase :: PARAM_DFLT => 0,
ApiBase :: PARAM_TYPE => 'namespace',
- ApiBase :: PARAM_ISMULTI => true,
+ ApiBase :: PARAM_ISMULTI => true,
),
'what' => array (
ApiBase :: PARAM_DFLT => 'title',
@@ -145,7 +151,6 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQuerySearch.php 30222 2008-01-28 19:05:26Z catrope $';
+ return __CLASS__ . ': $Id: ApiQuerySearch.php 35098 2008-05-20 17:13:28Z ialex $';
}
}
-
diff --git a/includes/api/ApiQuerySiteinfo.php b/includes/api/ApiQuerySiteinfo.php
index 81af7997..1fd3b888 100644
--- a/includes/api/ApiQuerySiteinfo.php
+++ b/includes/api/ApiQuerySiteinfo.php
@@ -23,218 +23,278 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
+if( !defined('MEDIAWIKI') ) {
// Eclipse helper - will be ignored in production
- require_once ('ApiQueryBase.php');
+ require_once( 'ApiQueryBase.php' );
}
/**
* A query action to return meta information about the wiki site.
- *
- * @addtogroup API
+ *
+ * @ingroup API
*/
class ApiQuerySiteinfo extends ApiQueryBase {
- public function __construct($query, $moduleName) {
- parent :: __construct($query, $moduleName, 'si');
+ public function __construct( $query, $moduleName ) {
+ parent :: __construct( $query, $moduleName, 'si' );
}
public function execute() {
-
$params = $this->extractRequestParams();
-
- foreach ($params['prop'] as $p) {
- switch ($p) {
- default :
- ApiBase :: dieDebug(__METHOD__, "Unknown prop=$p");
- case 'general' :
- $this->appendGeneralInfo($p);
+ foreach( $params['prop'] as $p )
+ {
+ switch ( $p )
+ {
+ case 'general':
+ $this->appendGeneralInfo( $p );
+ break;
+ case 'namespaces':
+ $this->appendNamespaces( $p );
break;
- case 'namespaces' :
- $this->appendNamespaces($p);
+ case 'namespacealiases':
+ $this->appendNamespaceAliases( $p );
break;
- case 'namespacealiases' :
- $this->appendNamespaceAliases($p);
+ case 'specialpagealiases':
+ $this->appendSpecialPageAliases( $p );
break;
- case 'interwikimap' :
- $filteriw = isset($params['filteriw']) ? $params['filteriw'] : false;
- $this->appendInterwikiMap($p, $filteriw);
+ case 'interwikimap':
+ $filteriw = isset( $params['filteriw'] ) ? $params['filteriw'] : false;
+ $this->appendInterwikiMap( $p, $filteriw );
break;
- case 'dbrepllag' :
- $this->appendDbReplLagInfo($p, $params['showalldb']);
+ case 'dbrepllag':
+ $this->appendDbReplLagInfo( $p, $params['showalldb'] );
break;
- case 'statistics' :
- $this->appendStatistics($p);
+ case 'statistics':
+ $this->appendStatistics( $p );
break;
+ case 'usergroups':
+ $this->appendUserGroups( $p );
+ break;
+ default :
+ ApiBase :: dieDebug( __METHOD__, "Unknown prop=$p" );
}
}
}
- protected function appendGeneralInfo($property) {
- global $wgSitename, $wgVersion, $wgCapitalLinks, $wgRightsCode, $wgRightsText, $wgLanguageCode, $IP;
-
- $data = array ();
+ protected function appendGeneralInfo( $property ) {
+ global $wgSitename, $wgVersion, $wgCapitalLinks, $wgRightsCode, $wgRightsText, $wgContLang;
+ global $wgLanguageCode, $IP, $wgEnableWriteAPI, $wgLang, $wgLocaltimezone, $wgLocalTZoffset;
+
+ $data = array();
$mainPage = Title :: newFromText(wfMsgForContent('mainpage'));
- $data['mainpage'] = $mainPage->getText();
+ $data['mainpage'] = $mainPage->getPrefixedText();
$data['base'] = $mainPage->getFullUrl();
$data['sitename'] = $wgSitename;
$data['generator'] = "MediaWiki $wgVersion";
- $svn = SpecialVersion::getSvnRevision ( $IP );
- if ( $svn ) $data['rev'] = $svn;
+ $svn = SpecialVersion::getSvnRevision( $IP );
+ if( $svn )
+ $data['rev'] = $svn;
$data['case'] = $wgCapitalLinks ? 'first-letter' : 'case-sensitive'; // 'case-insensitive' option is reserved for future
- if (isset($wgRightsCode))
+
+ if( isset( $wgRightsCode ) )
$data['rightscode'] = $wgRightsCode;
$data['rights'] = $wgRightsText;
$data['lang'] = $wgLanguageCode;
+ if( $wgContLang->isRTL() )
+ $data['rtl'] = '';
+ $data['fallback8bitEncoding'] = $wgLang->fallback8bitEncoding();
- $this->getResult()->addValue('query', $property, $data);
+ if( wfReadOnly() )
+ $data['readonly'] = '';
+ if( $wgEnableWriteAPI )
+ $data['writeapi'] = '';
+
+ $tz = $wgLocaltimezone;
+ $offset = $wgLocalTZoffset;
+ if( is_null( $tz ) ) {
+ $tz = 'UTC';
+ $offset = 0;
+ } elseif( is_null( $offset ) ) {
+ $offset = 0;
+ }
+ $data['timezone'] = $tz;
+ $data['timeoffset'] = $offset;
+
+ $this->getResult()->addValue( 'query', $property, $data );
}
-
- protected function appendNamespaces($property) {
+
+ protected function appendNamespaces( $property ) {
global $wgContLang;
-
- $data = array ();
- foreach ($wgContLang->getFormattedNamespaces() as $ns => $title) {
- $data[$ns] = array (
+ $data = array();
+ foreach( $wgContLang->getFormattedNamespaces() as $ns => $title )
+ {
+ $data[$ns] = array(
'id' => $ns
);
- ApiResult :: setContent($data[$ns], $title);
+ ApiResult :: setContent( $data[$ns], $title );
+ if( MWNamespace::hasSubpages($ns) )
+ $data[$ns]['subpages'] = '';
}
-
- $this->getResult()->setIndexedTagName($data, 'ns');
- $this->getResult()->addValue('query', $property, $data);
+
+ $this->getResult()->setIndexedTagName( $data, 'ns' );
+ $this->getResult()->addValue( 'query', $property, $data );
}
-
- protected function appendNamespaceAliases($property) {
+
+ protected function appendNamespaceAliases( $property ) {
global $wgNamespaceAliases;
-
- $data = array ();
- foreach ($wgNamespaceAliases as $title => $ns) {
- $item = array (
+ $data = array();
+ foreach( $wgNamespaceAliases as $title => $ns ) {
+ $item = array(
'id' => $ns
);
- ApiResult :: setContent($item, strtr($title, '_', ' '));
+ ApiResult :: setContent( $item, strtr( $title, '_', ' ' ) );
$data[] = $item;
}
-
- $this->getResult()->setIndexedTagName($data, 'ns');
- $this->getResult()->addValue('query', $property, $data);
+
+ $this->getResult()->setIndexedTagName( $data, 'ns' );
+ $this->getResult()->addValue( 'query', $property, $data );
}
-
- protected function appendInterwikiMap($property, $filter) {
- $this->resetQueryParams();
- $this->addTables('interwiki');
- $this->addFields(array('iw_prefix', 'iw_local', 'iw_url'));
-
- if($filter === 'local') {
- $this->addWhere('iw_local = 1');
- } elseif($filter === '!local') {
- $this->addWhere('iw_local = 0');
- } elseif($filter !== false) {
- ApiBase :: dieDebug(__METHOD__, "Unknown filter=$filter");
+ protected function appendSpecialPageAliases( $property ) {
+ global $wgLang;
+ $data = array();
+ foreach( $wgLang->getSpecialPageAliases() as $specialpage => $aliases )
+ {
+ $arr = array( 'realname' => $specialpage, 'aliases' => $aliases );
+ $this->getResult()->setIndexedTagName( $arr['aliases'], 'alias' );
+ $data[] = $arr;
}
+ $this->getResult()->setIndexedTagName( $data, 'specialpage' );
+ $this->getResult()->addValue( 'query', $property, $data );
+ }
+
+ protected function appendInterwikiMap( $property, $filter ) {
+ $this->resetQueryParams();
+ $this->addTables( 'interwiki' );
+ $this->addFields( array( 'iw_prefix', 'iw_local', 'iw_url' ) );
+
+ if( $filter === 'local' )
+ $this->addWhere( 'iw_local = 1' );
+ elseif( $filter === '!local' )
+ $this->addWhere( 'iw_local = 0' );
+ elseif( $filter !== false )
+ ApiBase :: dieDebug( __METHOD__, "Unknown filter=$filter" );
+
+ $this->addOption( 'ORDER BY', 'iw_prefix' );
- $this->addOption('ORDER BY', 'iw_prefix');
-
$db = $this->getDB();
- $res = $this->select(__METHOD__);
+ $res = $this->select( __METHOD__ );
$data = array();
- while($row = $db->fetchObject($res))
+ $langNames = Language::getLanguageNames();
+ while( $row = $db->fetchObject($res) )
{
+ $val = array();
$val['prefix'] = $row->iw_prefix;
- if ($row->iw_local == '1')
+ if( $row->iw_local == '1' )
$val['local'] = '';
// $val['trans'] = intval($row->iw_trans); // should this be exposed?
+ if( isset( $langNames[$row->iw_prefix] ) )
+ $val['language'] = $langNames[$row->iw_prefix];
$val['url'] = $row->iw_url;
-
+
$data[] = $val;
}
- $db->freeResult($res);
-
- $this->getResult()->setIndexedTagName($data, 'iw');
- $this->getResult()->addValue('query', $property, $data);
+ $db->freeResult( $res );
+
+ $this->getResult()->setIndexedTagName( $data, 'iw' );
+ $this->getResult()->addValue( 'query', $property, $data );
}
-
- protected function appendDbReplLagInfo($property, $includeAll) {
- global $wgLoadBalancer, $wgShowHostnames;
+ protected function appendDbReplLagInfo( $property, $includeAll ) {
+ global $wgShowHostnames;
$data = array();
-
- if ($includeAll) {
- if (!$wgShowHostnames)
+ if( $includeAll ) {
+ if ( !$wgShowHostnames )
$this->dieUsage('Cannot view all servers info unless $wgShowHostnames is true', 'includeAllDenied');
-
- global $wgDBservers;
- $lags = $wgLoadBalancer->getLagTimes();
+
+ $lb = wfGetLB();
+ $lags = $lb->getLagTimes();
foreach( $lags as $i => $lag ) {
- $data[] = array (
- 'host' => $wgDBservers[$i]['host'],
- 'lag' => $lag);
+ $data[] = array(
+ 'host' => $lb->getServerName( $i ),
+ 'lag' => $lag
+ );
}
} else {
- list( $host, $lag ) = $wgLoadBalancer->getMaxLag();
- $data[] = array (
+ list( $host, $lag ) = wfGetLB()->getMaxLag();
+ $data[] = array(
'host' => $wgShowHostnames ? $host : '',
- 'lag' => $lag);
- }
+ 'lag' => $lag
+ );
+ }
$result = $this->getResult();
- $result->setIndexedTagName($data, 'db');
- $result->addValue('query', $property, $data);
- }
-
- protected function appendStatistics($property) {
- $data = array ();
- $data['pages'] = intval(SiteStats::pages());
- $data['articles'] = intval(SiteStats::articles());
- $data['views'] = intval(SiteStats::views());
- $data['edits'] = intval(SiteStats::edits());
- $data['images'] = intval(SiteStats::images());
- $data['users'] = intval(SiteStats::users());
- $data['admins'] = intval(SiteStats::admins());
- $data['jobs'] = intval(SiteStats::jobs());
- $this->getResult()->addValue('query', $property, $data);
- }
-
+ $result->setIndexedTagName( $data, 'db' );
+ $result->addValue( 'query', $property, $data );
+ }
+
+ protected function appendStatistics( $property ) {
+ $data = array();
+ $data['pages'] = intval( SiteStats::pages() );
+ $data['articles'] = intval( SiteStats::articles() );
+ $data['views'] = intval( SiteStats::views() );
+ $data['edits'] = intval( SiteStats::edits() );
+ $data['images'] = intval( SiteStats::images() );
+ $data['users'] = intval( SiteStats::users() );
+ $data['admins'] = intval( SiteStats::admins() );
+ $data['jobs'] = intval( SiteStats::jobs() );
+ $this->getResult()->addValue( 'query', $property, $data );
+ }
+
+ protected function appendUserGroups( $property ) {
+ global $wgGroupPermissions;
+ $data = array();
+ foreach( $wgGroupPermissions as $group => $permissions ) {
+ $arr = array( 'name' => $group, 'rights' => array_keys( $permissions, true ) );
+ $this->getResult()->setIndexedTagName( $arr['rights'], 'permission' );
+ $data[] = $arr;
+ }
+
+ $this->getResult()->setIndexedTagName( $data, 'group' );
+ $this->getResult()->addValue( 'query', $property, $data );
+ }
+
public function getAllowedParams() {
- return array (
-
- 'prop' => array (
+ return array(
+ 'prop' => array(
ApiBase :: PARAM_DFLT => 'general',
ApiBase :: PARAM_ISMULTI => true,
- ApiBase :: PARAM_TYPE => array (
+ ApiBase :: PARAM_TYPE => array(
'general',
'namespaces',
'namespacealiases',
+ 'specialpagealiases',
'interwikimap',
'dbrepllag',
'statistics',
- )),
-
- 'filteriw' => array (
- ApiBase :: PARAM_TYPE => array (
+ 'usergroups',
+ )
+ ),
+ 'filteriw' => array(
+ ApiBase :: PARAM_TYPE => array(
'local',
'!local',
- )),
-
+ )
+ ),
'showalldb' => false,
);
}
public function getParamDescription() {
- return array (
- 'prop' => array (
+ return array(
+ 'prop' => array(
'Which sysinfo properties to get:',
' "general" - Overall system information',
' "namespaces" - List of registered namespaces (localized)',
' "namespacealiases" - List of registered namespace aliases',
+ ' "specialpagealiases" - List of special page 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',
),
'filteriw' => 'Return only local or only nonlocal entries of the interwiki map',
'showalldb' => 'List all database servers, not just the one lagging the most',
@@ -254,6 +314,6 @@ class ApiQuerySiteinfo extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQuerySiteinfo.php 30484 2008-02-03 19:29:59Z btongminh $';
+ return __CLASS__ . ': $Id: ApiQuerySiteinfo.php 37034 2008-07-04 09:21:11Z vasilievvv $';
}
}
diff --git a/includes/api/ApiQueryUserContributions.php b/includes/api/ApiQueryUserContributions.php
index 57d51cdb..c477acdb 100644
--- a/includes/api/ApiQueryUserContributions.php
+++ b/includes/api/ApiQueryUserContributions.php
@@ -30,8 +30,8 @@ if (!defined('MEDIAWIKI')) {
/**
* This query action adds a list of a specified user's contributions to the output.
- *
- * @addtogroup API
+ *
+ * @ingroup API
*/
class ApiQueryContributions extends ApiQueryBase {
@@ -48,7 +48,7 @@ class ApiQueryContributions extends ApiQueryBase {
// Parse some parameters
$this->params = $this->extractRequestParams();
- $prop = array_flip($this->params['prop']);
+ $prop = array_flip($this->params['prop']);
$this->fld_ids = isset($prop['ids']);
$this->fld_title = isset($prop['title']);
$this->fld_comment = isset($prop['comment']);
@@ -59,12 +59,20 @@ class ApiQueryContributions extends ApiQueryBase {
$this->selectNamedDB('contributions', DB_SLAVE, 'contributions');
$db = $this->getDB();
- // Prepare query
- $this->usernames = array();
- if(!is_array($this->params['user']))
- $this->params['user'] = array($this->params['user']);
- foreach($this->params['user'] as $u)
- $this->prepareUsername($u);
+ if(isset($this->params['userprefix']))
+ {
+ $this->prefixMode = true;
+ $this->userprefix = $this->params['userprefix'];
+ }
+ else
+ {
+ $this->usernames = array();
+ if(!is_array($this->params['user']))
+ $this->params['user'] = array($this->params['user']);
+ foreach($this->params['user'] as $u)
+ $this->prepareUsername($u);
+ $this->prefixMode = false;
+ }
$this->prepareQuery();
//Do the actual query.
@@ -114,7 +122,7 @@ class ApiQueryContributions extends ApiQueryBase {
$this->dieUsage( 'User parameter may not be empty', 'param_user' );
}
}
-
+
/**
* Prepares the query and returns the limit of rows requested
*/
@@ -122,14 +130,20 @@ class ApiQueryContributions extends ApiQueryBase {
//We're after the revision table, and the corresponding page row for
//anything we retrieve.
- list ($tbl_page, $tbl_revision) = $this->getDB()->tableNamesN('page', 'revision');
- $this->addTables("$tbl_revision LEFT OUTER JOIN $tbl_page ON page_id=rev_page");
-
+ $this->addTables(array('revision', 'page'));
+ $this->addWhere('page_id=rev_page');
+
$this->addWhereFld('rev_deleted', 0);
// We only want pages by the specified users.
- $this->addWhereFld( 'rev_user_text', $this->usernames );
+ if($this->prefixMode)
+ $this->addWhere("rev_user_text LIKE '" . $this->getDb()->escapeLike($this->userprefix) . "%'");
+ else
+ $this->addWhereFld( 'rev_user_text', $this->usernames );
// ... and in the specified timeframe.
- $this->addWhereRange('rev_timestamp',
+ // Ensure the same sort order for rev_user_text and rev_timestamp
+ // so our query is indexed
+ $this->addWhereRange('rev_user_text', $this->params['dir'], null, null);
+ $this->addWhereRange('rev_timestamp',
$this->params['dir'], $this->params['start'], $this->params['end'] );
$this->addWhereFld('page_namespace', $this->params['namespace']);
@@ -146,22 +160,23 @@ class ApiQueryContributions extends ApiQueryBase {
// Mandatory fields: timestamp allows request continuation
// ns+title checks if the user has access rights for this page
- // user_text is necessary if multiple users were specified
+ // user_text is necessary if multiple users were specified
$this->addFields(array(
'rev_timestamp',
'page_namespace',
'page_title',
'rev_user_text',
));
-
+
$this->addFieldsIf('rev_page', $this->fld_ids);
- $this->addFieldsIf('rev_id', $this->fld_ids);
+ $this->addFieldsIf('rev_id', $this->fld_ids || $this->fld_flags);
+ $this->addFieldsIf('page_latest', $this->fld_flags);
// $this->addFieldsIf('rev_text_id', $this->fld_ids); // Should this field be exposed?
$this->addFieldsIf('rev_comment', $this->fld_comment);
$this->addFieldsIf('rev_minor_edit', $this->fld_flags);
$this->addFieldsIf('page_is_new', $this->fld_flags);
}
-
+
/**
* Extract fields from the database row and append them to a result array
*/
@@ -172,12 +187,12 @@ class ApiQueryContributions extends ApiQueryBase {
$vals['user'] = $row->rev_user_text;
if ($this->fld_ids) {
$vals['pageid'] = intval($row->rev_page);
- $vals['revid'] = intval($row->rev_id);
+ $vals['revid'] = intval($row->rev_id);
// $vals['textid'] = intval($row->rev_text_id); // todo: Should this field be exposed?
}
-
+
if ($this->fld_title)
- ApiQueryBase :: addTitleInfo($vals,
+ ApiQueryBase :: addTitleInfo($vals,
Title :: makeTitle($row->page_namespace, $row->page_title));
if ($this->fld_timestamp)
@@ -188,6 +203,8 @@ class ApiQueryContributions extends ApiQueryBase {
$vals['new'] = '';
if ($row->rev_minor_edit)
$vals['minor'] = '';
+ if ($row->page_latest == $row->rev_id)
+ $vals['top'] = '';
}
if ($this->fld_comment && !empty ($row->rev_comment))
@@ -214,6 +231,7 @@ class ApiQueryContributions extends ApiQueryBase {
'user' => array (
ApiBase :: PARAM_ISMULTI => true
),
+ 'userprefix' => null,
'dir' => array (
ApiBase :: PARAM_DFLT => 'older',
ApiBase :: PARAM_TYPE => array (
@@ -252,6 +270,7 @@ class ApiQueryContributions extends ApiQueryBase {
'start' => 'The start timestamp to return from.',
'end' => 'The end timestamp to return to.',
'user' => 'The user to retrieve contributions for.',
+ 'userprefix' => 'Retrieve contibutions for all users whose names begin with this value. Overrides ucuser.',
'dir' => 'The direction to search (older or newer).',
'namespace' => 'Only list contributions in these namespaces',
'prop' => 'Include additional pieces of information',
@@ -265,12 +284,12 @@ class ApiQueryContributions extends ApiQueryBase {
protected function getExamples() {
return array (
- 'api.php?action=query&list=usercontribs&ucuser=YurikBot'
+ 'api.php?action=query&list=usercontribs&ucuser=YurikBot',
+ 'api.php?action=query&list=usercontribs&ucuserprefix=217.121.114.',
);
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryUserContributions.php 30578 2008-02-05 15:40:58Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryUserContributions.php 37383 2008-07-09 11:44:49Z btongminh $';
}
}
-
diff --git a/includes/api/ApiQueryUserInfo.php b/includes/api/ApiQueryUserInfo.php
index 010d9f4f..2d55a352 100644
--- a/includes/api/ApiQueryUserInfo.php
+++ b/includes/api/ApiQueryUserInfo.php
@@ -30,8 +30,8 @@ if (!defined('MEDIAWIKI')) {
/**
* Query module to get information about the currently logged-in user
- *
- * @addtogroup API
+ *
+ * @ingroup API
*/
class ApiQueryUserInfo extends ApiQueryBase {
@@ -52,7 +52,7 @@ class ApiQueryUserInfo extends ApiQueryBase {
$r = $this->getCurrentUserInfo();
$result->addValue("query", $this->getModuleName(), $r);
}
-
+
protected function getCurrentUserInfo() {
global $wgUser;
$result = $this->getResult();
@@ -67,7 +67,7 @@ class ApiQueryUserInfo extends ApiQueryBase {
$vals['blockedby'] = User::whoIs($wgUser->blockedBy());
$vals['blockreason'] = $wgUser->blockedFor();
}
- }
+ }
if (isset($this->prop['hasmsg']) && $wgUser->getNewtalk()) {
$vals['messages'] = '';
}
@@ -90,13 +90,13 @@ class ApiQueryUserInfo extends ApiQueryBase {
}
return $vals;
}
-
+
protected function getRateLimits()
{
global $wgUser, $wgRateLimits;
if(!$wgUser->isPingLimitable())
return array(); // No limits
-
+
// Find out which categories we belong to
$categories = array();
if($wgUser->isAnon())
@@ -110,7 +110,7 @@ class ApiQueryUserInfo extends ApiQueryBase {
if(!$wgUser->isAnon())
$categories[] = 'newbie';
}
-
+
// Now get the actual limits
$retval = array();
foreach($wgRateLimits as $action => $limits)
@@ -121,7 +121,7 @@ class ApiQueryUserInfo extends ApiQueryBase {
$retval[$action][$cat]['seconds'] = $limits[$cat][1];
}
return $retval;
- }
+ }
public function getAllowedParams() {
return array (
@@ -168,6 +168,6 @@ class ApiQueryUserInfo extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryUserInfo.php 30395 2008-02-01 14:46:46Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryUserInfo.php 35186 2008-05-22 16:39:43Z brion $';
}
}
diff --git a/includes/api/ApiQueryUsers.php b/includes/api/ApiQueryUsers.php
index 144bfba2..a8147567 100644
--- a/includes/api/ApiQueryUsers.php
+++ b/includes/api/ApiQueryUsers.php
@@ -30,10 +30,10 @@ if (!defined('MEDIAWIKI')) {
/**
* Query module to get information about a list of users
- *
- * @addtogroup API
+ *
+ * @ingroup API
*/
-
+
class ApiQueryUsers extends ApiQueryBase {
public function __construct($query, $moduleName) {
@@ -50,7 +50,7 @@ if (!defined('MEDIAWIKI')) {
} else {
$this->prop = array();
}
-
+
if(is_array($params['users'])) {
$r = $this->getOtherUsersInfo($params['users']);
$result->setIndexedTagName($r, 'user');
@@ -63,38 +63,44 @@ if (!defined('MEDIAWIKI')) {
// Canonicalize user names
foreach($users as $u) {
$n = User::getCanonicalName($u);
- if($n === false)
+ if($n === false || $n === '')
$retval[] = array('name' => $u, 'invalid' => '');
else
$goodNames[] = $n;
}
+ if(empty($goodNames))
+ return $retval;
$db = $this->getDb();
- $userTable = $db->tableName('user');
- $tables = "$userTable AS u1";
+ $this->addTables('user', 'u1');
$this->addFields('u1.user_name');
$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'])) {
- $ug = $db->tableName('user_groups');
- $tables = "$tables LEFT JOIN $ug ON ug_user=u1.user_id";
+ $this->addTables('user_groups');
+ $this->addJoinConds(array('user_groups' => array('LEFT JOIN', 'ug_user=u1.user_id')));
$this->addFields('ug_group');
}
if(isset($this->prop['blockinfo'])) {
- $ipb = $db->tableName('ipblocks');
- $tables = "$tables LEFT JOIN $ipb ON ipb_user=u1.user_id";
- $tables = "$tables LEFT JOIN $userTable AS u2 ON ipb_by=u2.user_id";
- $this->addFields(array('ipb_reason', 'u2.user_name AS blocker_name'));
+ $this->addTables('ipblocks');
+ $this->addTables('user', 'u2');
+ $u2 = $this->getAliasedName('user', 'u2');
+ $this->addJoinConds(array(
+ 'ipblocks' => array('LEFT JOIN', 'ipb_user=u1.user_id'),
+ $u2 => array('LEFT JOIN', 'ipb_by=u2.user_id')));
+ $this->addFields(array('ipb_reason', 'u2.user_name blocker_name'));
}
- $this->addTables($tables);
-
+
$data = array();
$res = $this->select(__METHOD__);
while(($r = $db->fetchObject($res))) {
$data[$r->user_name]['name'] = $r->user_name;
if(isset($this->prop['editcount']))
$data[$r->user_name]['editcount'] = $r->user_editcount;
+ if(isset($this->prop['registration']))
+ $data[$r->user_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))
@@ -105,7 +111,7 @@ if (!defined('MEDIAWIKI')) {
$data[$r->user_name]['blockreason'] = $r->ipb_reason;
}
}
-
+
// Second pass: add result data to $retval
foreach($goodNames as $u) {
if(!isset($data[$u]))
@@ -116,7 +122,7 @@ if (!defined('MEDIAWIKI')) {
$retval[] = $data[$u];
}
}
- return $retval;
+ return $retval;
}
public function getAllowedParams() {
@@ -127,7 +133,8 @@ if (!defined('MEDIAWIKI')) {
ApiBase :: PARAM_TYPE => array (
'blockinfo',
'groups',
- 'editcount'
+ 'editcount',
+ 'registration'
)
),
'users' => array(
@@ -157,6 +164,6 @@ if (!defined('MEDIAWIKI')) {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryUserInfo.php 30128 2008-01-24 17:59:07Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryUsers.php 38183 2008-07-29 12:58:04Z rotem $';
}
}
diff --git a/includes/api/ApiQueryWatchlist.php b/includes/api/ApiQueryWatchlist.php
index 91a0c951..d17e83f6 100644
--- a/includes/api/ApiQueryWatchlist.php
+++ b/includes/api/ApiQueryWatchlist.php
@@ -31,8 +31,8 @@ if (!defined('MEDIAWIKI')) {
/**
* This query action allows clients to retrieve a list of recently modified pages
* that are part of the logged-in user's watchlist.
- *
- * @addtogroup API
+ *
+ * @ingroup API
*/
class ApiQueryWatchlist extends ApiQueryGeneratorBase {
@@ -50,7 +50,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
private $fld_ids = false, $fld_title = false, $fld_patrol = false, $fld_flags = false,
$fld_timestamp = false, $fld_user = false, $fld_comment = false, $fld_sizes = false;
-
+
private function run($resultPageSet = null) {
global $wgUser, $wgDBtype;
@@ -122,7 +122,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
'recentchanges'
));
- $userId = $wgUser->getID();
+ $userId = $wgUser->getId();
$this->addWhere(array (
'wl_namespace = rc_namespace',
'wl_title = rc_title',
@@ -134,15 +134,15 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
$this->addWhereRange('rc_timestamp', $dir, $start, $end);
$this->addWhereFld('wl_namespace', $namespace);
$this->addWhereIf('rc_this_oldid=page_latest', !$allrev);
-
+
if (!is_null($show)) {
$show = array_flip($show);
/* Check for conflicting parameters. */
- if ((isset ($show['minor']) && isset ($show['!minor']))
- || (isset ($show['bot']) && isset ($show['!bot']))
+ if ((isset ($show['minor']) && isset ($show['!minor']))
+ || (isset ($show['bot']) && isset ($show['!bot']))
|| (isset ($show['anon']) && isset ($show['!anon']))) {
-
+
$this->dieUsage("Incorrect parameter - mutually exclusive values may not be supplied", 'show');
}
@@ -155,7 +155,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
$this->addWhereIf('rc_user != 0', isset ($show['!anon']));
}
-
+
# This is an index optimization for mysql, as done in the Special:Watchlist page
$this->addWhereIf("rc_timestamp > ''", !isset ($start) && !isset ($end) && $wgDBtype == 'mysql');
@@ -205,9 +205,9 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
if ($this->fld_ids) {
$vals['pageid'] = intval($row->rc_cur_id);
- $vals['revid'] = intval($row->rc_this_oldid);
+ $vals['revid'] = intval($row->rc_this_oldid);
}
-
+
if ($this->fld_title)
ApiQueryBase :: addTitleInfo($vals, Title :: makeTitle($row->rc_namespace, $row->rc_title));
@@ -305,7 +305,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
'end' => 'The timestamp to end enumerating.',
'namespace' => 'Filter changes to only the given namespace(s).',
'dir' => 'In which direction to enumerate pages.',
- 'limit' => 'How many total pages to return per request.',
+ 'limit' => 'How many total results to return per request.',
'prop' => 'Which additional items to get (non-generator mode only).',
'show' => array (
'Show only items that meet this criteria.',
@@ -315,7 +315,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
}
public function getDescription() {
- return '';
+ return "Get all recent changes to pages in the logged in user's watchlist";
}
protected function getExamples() {
@@ -329,7 +329,6 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryWatchlist.php 30222 2008-01-28 19:05:26Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryWatchlist.php 37909 2008-07-22 13:26:15Z catrope $';
}
}
-
diff --git a/includes/api/ApiResult.php b/includes/api/ApiResult.php
index ffab51ef..9e798d35 100644
--- a/includes/api/ApiResult.php
+++ b/includes/api/ApiResult.php
@@ -33,17 +33,17 @@ if (!defined('MEDIAWIKI')) {
* It simply wraps a nested array() structure, adding some functions to simplify array's modifications.
* As various modules execute, they add different pieces of information to this result,
* structuring it as it will be given to the client.
- *
+ *
* Each subarray may either be a dictionary - key-value pairs with unique keys,
* or lists, where the items are added using $data[] = $value notation.
- *
+ *
* There are two special key values that change how XML output is generated:
* '_element' This key sets the tag name for the rest of the elements in the current array.
* It is only inserted if the formatter returned true for getNeedsRawData()
* '*' This key has special meaning only to the XML formatter, and is outputed as is
- * for all others. In XML it becomes the content of the current element.
- *
- * @addtogroup API
+ * for all others. In XML it becomes the content of the current element.
+ *
+ * @ingroup API
*/
class ApiResult extends ApiBase {
@@ -64,15 +64,15 @@ class ApiResult extends ApiBase {
public function reset() {
$this->mData = array ();
}
-
+
/**
- * Call this function when special elements such as '_element'
- * are needed by the formatter, for example in XML printing.
+ * Call this function when special elements such as '_element'
+ * are needed by the formatter, for example in XML printing.
*/
public function setRawMode() {
$this->mIsRawMode = true;
}
-
+
/**
* Returns true if the result is being created for the formatter that requested raw data.
*/
@@ -139,7 +139,7 @@ class ApiResult extends ApiBase {
// Do not use setElement() as it is ok to call this more than once
$arr['_element'] = $tag;
}
-
+
/**
* Calls setIndexedTagName() on $arr and each sub-array
*/
@@ -147,7 +147,7 @@ class ApiResult extends ApiBase {
{
if(!is_array($arr))
return;
- foreach($arr as $a)
+ foreach($arr as &$a)
{
if(!is_array($a))
continue;
@@ -160,7 +160,7 @@ class ApiResult extends ApiBase {
* Add value to the output data at the given path.
* Path is an indexed array, each element specifing the branch at which to add the new value
* Setting $path to array('a','b','c') is equivalent to data['a']['b']['c'] = $value
- * If $name is empty, the $value is added as a next list element data[] = $value
+ * If $name is empty, the $value is added as a next list element data[] = $value
*/
public function addValue($path, $name, $value) {
@@ -191,7 +191,7 @@ class ApiResult extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiResult.php 26855 2007-10-20 18:27:39Z catrope $';
+ return __CLASS__ . ': $Id: ApiResult.php 35098 2008-05-20 17:13:28Z ialex $';
}
}
@@ -199,17 +199,17 @@ class ApiResult extends ApiBase {
if (!function_exists('array_intersect_key')) {
function array_intersect_key($isec, $keys) {
$argc = func_num_args();
-
+
if ($argc > 2) {
for ($i = 1; !empty($isec) && $i < $argc; $i++) {
$arr = func_get_arg($i);
-
+
foreach (array_keys($isec) as $key) {
- if (!isset($arr[$key]))
+ if (!isset($arr[$key]))
unset($isec[$key]);
}
}
-
+
return $isec;
} else {
$res = array();
@@ -217,7 +217,7 @@ if (!function_exists('array_intersect_key')) {
if (isset($keys[$key]))
$res[$key] = $isec[$key];
}
-
+
return $res;
}
}
diff --git a/includes/api/ApiRollback.php b/includes/api/ApiRollback.php
index d714f99c..3739f694 100644
--- a/includes/api/ApiRollback.php
+++ b/includes/api/ApiRollback.php
@@ -28,7 +28,7 @@ if (!defined('MEDIAWIKI')) {
}
/**
- * @addtogroup API
+ * @ingroup API
*/
class ApiRollback extends ApiBase {
@@ -40,7 +40,7 @@ class ApiRollback extends ApiBase {
global $wgUser;
$this->getMain()->requestWriteMode();
$params = $this->extractRequestParams();
-
+
$titleObj = NULL;
if(!isset($params['title']))
$this->dieUsageMsg(array('missingparam', 'title'));
@@ -62,15 +62,12 @@ class ApiRollback extends ApiBase {
$articleObj = new Article($titleObj);
$summary = (isset($params['summary']) ? $params['summary'] : "");
$details = null;
- $dbw = wfGetDb(DB_MASTER);
- $dbw->begin();
$retval = $articleObj->doRollback($username, $summary, $params['token'], $params['markbot'], $details);
if(!empty($retval))
// We don't care about multiple errors, just report one of them
$this->dieUsageMsg(current($retval));
- $dbw->commit();
$current = $target = $summary = NULL;
extract($details);
@@ -85,9 +82,9 @@ class ApiRollback extends ApiBase {
$this->getResult()->addValue(null, $this->getModuleName(), $info);
}
-
+
public function mustBePosted() { return true; }
-
+
public function getAllowedParams() {
return array (
'title' => null,
@@ -123,6 +120,6 @@ class ApiRollback extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiRollback.php 30222 2008-01-28 19:05:26Z catrope $';
+ return __CLASS__ . ': $Id: ApiRollback.php 35098 2008-05-20 17:13:28Z ialex $';
}
}
diff --git a/includes/api/ApiUnblock.php b/includes/api/ApiUnblock.php
index afbd3f0e..d6a02a2a 100644
--- a/includes/api/ApiUnblock.php
+++ b/includes/api/ApiUnblock.php
@@ -31,7 +31,7 @@ if (!defined('MEDIAWIKI')) {
* API module that facilitates the unblocking of users. Requires API write mode
* to be enabled.
*
- * @addtogroup API
+ * @ingroup API
*/
class ApiUnblock extends ApiBase {
@@ -41,7 +41,7 @@ class ApiUnblock extends ApiBase {
/**
* Unblocks the specified user or provides the reason the unblock failed.
- */
+ */
public function execute() {
global $wgUser;
$this->getMain()->requestWriteMode();
@@ -70,19 +70,16 @@ class ApiUnblock extends ApiBase {
$id = $params['id'];
$user = $params['user'];
$reason = (is_null($params['reason']) ? '' : $params['reason']);
- $dbw = wfGetDb(DB_MASTER);
- $dbw->begin();
$retval = IPUnblockForm::doUnblock($id, $user, $reason, $range);
if(!empty($retval))
$this->dieUsageMsg($retval);
- $dbw->commit();
$res['id'] = $id;
$res['user'] = $user;
$res['reason'] = $reason;
$this->getResult()->addValue(null, $this->getModuleName(), $res);
}
-
+
public function mustBePosted() { return true; }
public function getAllowedParams() {
@@ -119,6 +116,6 @@ class ApiUnblock extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiUnblock.php 30222 2008-01-28 19:05:26Z catrope $';
+ return __CLASS__ . ': $Id: ApiUnblock.php 35098 2008-05-20 17:13:28Z ialex $';
}
}
diff --git a/includes/api/ApiUndelete.php b/includes/api/ApiUndelete.php
index b27841a8..e054a70e 100644
--- a/includes/api/ApiUndelete.php
+++ b/includes/api/ApiUndelete.php
@@ -28,7 +28,7 @@ if (!defined('MEDIAWIKI')) {
}
/**
- * @addtogroup API
+ * @ingroup API
*/
class ApiUndelete extends ApiBase {
@@ -40,7 +40,7 @@ class ApiUndelete extends ApiBase {
global $wgUser;
$this->getMain()->requestWriteMode();
$params = $this->extractRequestParams();
-
+
$titleObj = NULL;
if(!isset($params['title']))
$this->dieUsageMsg(array('missingparam', 'title'));
@@ -61,6 +61,8 @@ class ApiUndelete extends ApiBase {
$this->dieUsageMsg(array('invalidtitle', $params['title']));
// Convert timestamps
+ if(!isset($params['timestamps']))
+ $params['timestamps'] = array();
if(!is_array($params['timestamps']))
$params['timestamps'] = array($params['timestamps']);
foreach($params['timestamps'] as $i => $ts)
@@ -73,16 +75,19 @@ class ApiUndelete extends ApiBase {
if(!is_array($retval))
$this->dieUsageMsg(array('cannotundelete'));
- $dbw->commit();
+ if($retval[1])
+ wfRunHooks( 'FileUndeleteComplete',
+ array($titleObj, array(), $wgUser, $params['reason']) );
+
$info['title'] = $titleObj->getPrefixedText();
$info['revisions'] = $retval[0];
$info['fileversions'] = $retval[1];
$info['reason'] = $retval[2];
$this->getResult()->addValue(null, $this->getModuleName(), $info);
}
-
+
public function mustBePosted() { return true; }
-
+
public function getAllowedParams() {
return array (
'title' => null,
@@ -118,6 +123,6 @@ class ApiUndelete extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiUndelete.php 30222 2008-01-28 19:05:26Z catrope $';
+ return __CLASS__ . ': $Id: ApiUndelete.php 35348 2008-05-26 10:51:31Z catrope $';
}
}
diff --git a/includes/cbt/CBTCompiler.php b/includes/cbt/CBTCompiler.php
index 17f43500..75955797 100644
--- a/includes/cbt/CBTCompiler.php
+++ b/includes/cbt/CBTCompiler.php
@@ -9,8 +9,8 @@
require_once( dirname( __FILE__ ) . '/CBTProcessor.php' );
-/**
- * Push a value onto the stack
+/**
+ * Push a value onto the stack
* Argument 1: value
*/
define( 'CBT_PUSH', 1 );
@@ -51,7 +51,7 @@ class CBTOp {
}
function name() {
- $opcodeNames = array(
+ $opcodeNames = array(
CBT_PUSH => 'PUSH',
CBT_CAT => 'CAT',
CBT_CATS => 'CATS',
@@ -102,7 +102,7 @@ class CBTCompiler {
} else {
$text = true;
}
-
+
return $text;
}
@@ -121,13 +121,13 @@ class CBTCompiler {
/**
* Recursive workhorse for text mode.
- *
- * Processes text mode starting from offset $p, until either $end is
- * reached or a closing brace is found. If $needClosing is false, a
+ *
+ * Processes text mode starting from offset $p, until either $end is
+ * reached or a closing brace is found. If $needClosing is false, a
* closing brace will flag an error, if $needClosing is true, the lack
- * of a closing brace will flag an error.
+ * of a closing brace will flag an error.
*
- * The parameter $p is advanced to the position after the closing brace,
+ * The parameter $p is advanced to the position after the closing brace,
* or after the end. A CBTValue is returned.
*
* @private
@@ -136,7 +136,7 @@ class CBTCompiler {
$in =& $this->mText;
$start = $p;
$atStart = true;
-
+
$foundClosing = false;
while ( $p < $end ) {
$matchLength = strcspn( $in, CBT_BRACE, $p, $end - $p );
@@ -162,9 +162,9 @@ class CBTCompiler {
$this->mOps[] = $this->op( CBT_CAT, substr( $in, $p, $matchLength ) );
}
- // Advance the pointer
+ // Advance the pointer
$p = $pToken + 1;
-
+
// Check for closing brace
if ( $in[$pToken] == '}' ) {
$foundClosing = true;
@@ -184,7 +184,7 @@ class CBTCompiler {
$atStart = false;
} else {
$this->mOps[] = $this->op( CBT_CATS );
- }
+ }
}
if ( $foundClosing && !$needClosing ) {
$this->error( 'Errant closing brace', $p );
@@ -200,12 +200,12 @@ class CBTCompiler {
/**
* Recursive workhorse for function mode.
*
- * Processes function mode starting from offset $p, until either $end is
- * reached or a closing brace is found. If $needClosing is false, a
+ * Processes function mode starting from offset $p, until either $end is
+ * reached or a closing brace is found. If $needClosing is false, a
* closing brace will flag an error, if $needClosing is true, the lack
- * of a closing brace will flag an error.
+ * of a closing brace will flag an error.
*
- * The parameter $p is advanced to the position after the closing brace,
+ * The parameter $p is advanced to the position after the closing brace,
* or after the end. A CBTValue is returned.
*
* @private
@@ -364,4 +364,3 @@ class CBTCompiler {
';
}
}
-
diff --git a/includes/cbt/CBTProcessor.php b/includes/cbt/CBTProcessor.php
index 31d1b60a..4fa1a93b 100644
--- a/includes/cbt/CBTProcessor.php
+++ b/includes/cbt/CBTProcessor.php
@@ -2,7 +2,7 @@
/**
* PHP version of the callback template processor
- * This is currently used as a test rig and is likely to be used for
+ * This is currently used as a test rig and is likely to be used for
* compatibility purposes later, where the C++ extension is not available.
*/
@@ -44,9 +44,9 @@ function cbt_value( $text = '', $deps = array(), $isTemplate = false ) {
/**
* A dependency-tracking value class
- * Callback functions should return one of these, unless they have
+ * Callback functions should return one of these, unless they have
* no dependencies in which case they can return a string.
- */
+ */
class CBTValue {
var $mText, $mDeps, $mIsTemplate;
@@ -175,7 +175,7 @@ class CBTProcessor {
/**
* Execute the template.
- * If $compile is true, produces an optimised template where functions with static
+ * If $compile is true, produces an optimised template where functions with static
* dependencies have been replaced by their return values.
*/
function execute( $compile = false ) {
@@ -204,7 +204,7 @@ class CBTProcessor {
$context = rtrim( str_replace( "\t", " ", substr( $this->mText, $startLine, $endLine - $startLine ) ) );
$text .= htmlspecialchars( $context ) . "\n" . str_repeat( ' ', $pos - $startLine ) . "^\n</pre>\n";
- }
+ }
wfProfileOut( $fname );
return $text;
}
@@ -223,7 +223,7 @@ class CBTProcessor {
return $this->doOpenText( $start, $end, false );
}
- /**
+ /**
* Escape text for a template if we are producing a template. Do nothing
* if we are producing plain text.
*/
@@ -237,13 +237,13 @@ class CBTProcessor {
/**
* Recursive workhorse for text mode.
- *
- * Processes text mode starting from offset $p, until either $end is
- * reached or a closing brace is found. If $needClosing is false, a
+ *
+ * Processes text mode starting from offset $p, until either $end is
+ * reached or a closing brace is found. If $needClosing is false, a
* closing brace will flag an error, if $needClosing is true, the lack
- * of a closing brace will flag an error.
+ * of a closing brace will flag an error.
*
- * The parameter $p is advanced to the position after the closing brace,
+ * The parameter $p is advanced to the position after the closing brace,
* or after the end. A CBTValue is returned.
*
* @private
@@ -254,7 +254,7 @@ class CBTProcessor {
$in =& $this->mText;
$start = $p;
$ret = new CBTValue( '', array(), $this->mCompiling );
-
+
$foundClosing = false;
while ( $p < $end ) {
$matchLength = strcspn( $in, CBT_BRACE, $p, $end - $p );
@@ -270,15 +270,15 @@ class CBTProcessor {
// Output the text before the brace
$ret->cat( substr( $in, $p, $matchLength ) );
- // Advance the pointer
+ // Advance the pointer
$p = $pToken + 1;
-
+
// Check for closing brace
if ( $in[$pToken] == '}' ) {
$foundClosing = true;
break;
}
-
+
// Handle the "{fn}" special case
if ( $pToken > 0 && $in[$pToken-1] == '"' ) {
wfProfileOut( $fname );
@@ -290,7 +290,7 @@ class CBTProcessor {
$ret->cat( $val );
} else {
// Process the function mode component
- wfProfileOut( $fname );
+ wfProfileOut( $fname );
$ret->cat( $this->doOpenFunction( $p, $end ) );
wfProfileIn( $fname );
}
@@ -300,19 +300,19 @@ class CBTProcessor {
} elseif ( !$foundClosing && $needClosing ) {
$this->error( 'Unclosed text section', $start );
}
- wfProfileOut( $fname );
+ wfProfileOut( $fname );
return $ret;
}
/**
* Recursive workhorse for function mode.
*
- * Processes function mode starting from offset $p, until either $end is
- * reached or a closing brace is found. If $needClosing is false, a
+ * Processes function mode starting from offset $p, until either $end is
+ * reached or a closing brace is found. If $needClosing is false, a
* closing brace will flag an error, if $needClosing is true, the lack
- * of a closing brace will flag an error.
+ * of a closing brace will flag an error.
*
- * The parameter $p is advanced to the position after the closing brace,
+ * The parameter $p is advanced to the position after the closing brace,
* or after the end. A CBTValue is returned.
*
* @private
@@ -383,8 +383,8 @@ class CBTProcessor {
}
}
- // The dynamic parts of the string are still represented as functions, and
- // function invocations have no dependencies. Thus the compiled result has
+ // The dynamic parts of the string are still represented as functions, and
+ // function invocations have no dependencies. Thus the compiled result has
// no dependencies.
$val = new CBTValue( "{{$compiled}}", array(), true );
}
@@ -393,7 +393,7 @@ class CBTProcessor {
/**
* Execute a function, caching and returning the result value.
- * $tokens is an array of CBTValue objects. $tokens[0] is the function
+ * $tokens is an array of CBTValue objects. $tokens[0] is the function
* name, the others are arguments. $p is the string position, and is used
* for error messages only.
*/
@@ -403,12 +403,12 @@ class CBTProcessor {
}
$fname = 'CBTProcessor::doFunction';
wfProfileIn( $fname );
-
+
$ret = new CBTValue;
-
+
// All functions implicitly depend on their arguments, and the function name
- // While this is not strictly necessary for all functions, it's true almost
- // all the time and so convenient to do automatically.
+ // While this is not strictly necessary for all functions, it's true almost
+ // all the time and so convenient to do automatically.
$ret->addDeps( $tokens );
$this->mCurrentPos = $p;
@@ -453,25 +453,25 @@ class CBTProcessor {
// If the output was a template, execute it
$val->execute( $this );
-
+
if ( $this->mCompiling ) {
// Escape any braces so that the output will be a valid template
$val->templateEscape();
- }
+ }
$val->removeDeps( $this->mIgnorableDeps );
$ret->addDeps( $val );
$ret->setText( $val->getText() );
if ( CBT_DEBUG ) {
- wfDebug( "doFunction $func args = "
- . var_export( $tokens, true )
- . "unexpanded return = "
+ wfDebug( "doFunction $func args = "
+ . var_export( $tokens, true )
+ . "unexpanded return = "
. var_export( $unexpanded, true )
- . "expanded return = "
- . var_export( $ret, true )
+ . "expanded return = "
+ . var_export( $ret, true )
);
}
-
+
wfProfileOut( $fname );
return $ret;
}
@@ -500,12 +500,12 @@ class CBTProcessor {
}
if ( $condition->getText() != '' ) {
- return new CBTValue( $trueBlock->getText(),
+ return new CBTValue( $trueBlock->getText(),
array_merge( $condition->getDeps(), $trueBlock->getDeps() ),
$trueBlock->mIsTemplate );
} else {
if ( !is_null( $falseBlock ) ) {
- return new CBTValue( $falseBlock->getText(),
+ return new CBTValue( $falseBlock->getText(),
array_merge( $condition->getDeps(), $falseBlock->getDeps() ),
$falseBlock->mIsTemplate );
} else {
@@ -529,12 +529,11 @@ class CBTProcessor {
return '}';
}
- /**
+ /**
* escape built-in.
- * Escape text for inclusion in an HTML attribute
+ * Escape text for inclusion in an HTML attribute
*/
function bi_escape( $val ) {
return new CBTValue( htmlspecialchars( $val->getText() ), $val->getDeps() );
}
}
-
diff --git a/includes/cbt/README b/includes/cbt/README
index cffcef2f..30581661 100644
--- a/includes/cbt/README
+++ b/includes/cbt/README
@@ -1,7 +1,7 @@
Overview
--------
-CBT (callback-based templates) is an experimental system for improving skin
+CBT (callback-based templates) is an experimental system for improving skin
rendering time in MediaWiki and similar applications. The fundamental concept is
a template language which contains tags which pull text from PHP callbacks.
These PHP callbacks do not simply return text, they also return a description of
@@ -15,24 +15,24 @@ efficiency gains and techniques. TemplateProcessor was the first element of this
experiment. It is a class written in PHP which parses a template, and produces
either an optimised template with dependencies removed, or the output text
itself. I found that even with a heavily optimised template, this processor was
-not fast enough to match the speed of the original MonoBook.
+not fast enough to match the speed of the original MonoBook.
To improve the efficiency, I wrote TemplateCompiler, which takes a template,
preferably pre-optimised by TemplateProcessor, and generates PHP code from it.
The generated code is a single expression, concatenating static text and
-callback results. This approach turned out to be very efficient, making
-significant time savings compared to the original MonoBook.
+callback results. This approach turned out to be very efficient, making
+significant time savings compared to the original MonoBook.
-Despite this success, the code has been shelved for the time being. There were
+Despite this success, the code has been shelved for the time being. There were
a number of unresolved implementation problems, and I felt that there were more
pressing priorities for MediaWiki development than solving them and bringing
-this module to completion. I also believe that more research is needed into
+this module to completion. I also believe that more research is needed into
other possible template architectures. There is nothing fundamentally wrong with
the CBT concept, and I would encourage others to continue its development.
The problems I saw were:
-* Extensibility. Can non-Wikimedia installations easily extend and modify CBT
+* Extensibility. Can non-Wikimedia installations easily extend and modify CBT
skins? Patching seems to be necessary, is this acceptable? MediaWiki
extensions are another problem. Unless the interfaces allow them to return
dependencies, any hooks will have to be marked dynamic and thus inefficient.
@@ -51,14 +51,14 @@ The problems I saw were:
Template syntax
---------------
-There are two modes: text mode and function mode. The brace characters "{"
+There are two modes: text mode and function mode. The brace characters "{"
and "}" are the only reserved characters. Either one of them will switch from
-text mode to function mode wherever they appear, no exceptions.
+text mode to function mode wherever they appear, no exceptions.
In text mode, all characters are passed through to the output. In function
-mode, text is split into tokens, delimited either by whitespace or by
+mode, text is split into tokens, delimited either by whitespace or by
matching pairs of braces. The first token is taken to be a function name. The
-other tokens are first processed in function mode themselves, then they are
+other tokens are first processed in function mode themselves, then they are
passed to the named function as parameters. The return value of the function
is passed through to the output.
@@ -68,39 +68,39 @@ Example:
First brace switches to function mode. The function name is escape, the first
and only parameter is {"hello"}. This parameter is executed. The braces around
the parameter cause the parser to switch to text mode, thus the string "hello",
-including the quotes, is passed back and used as an argument to the escape
-function.
+including the quotes, is passed back and used as an argument to the escape
+function.
Example:
{if title {<h1>{title}</h1>}}
-The function name is "if". The first parameter is the result of calling the
+The function name is "if". The first parameter is the result of calling the
function "title". The second parameter is a level 1 HTML heading containing
-the result of the function "title". "if" is a built-in function which will
+the result of the function "title". "if" is a built-in function which will
return the second parameter only if the first is non-blank, so the effect of
this is to return a heading element only if a title exists.
As a shortcut for generation of HTML attributes, if a function mode segment is
-surrounded by double quotes, quote characters in the return value will be
-escaped. This only applies if the quote character immediately precedes the
+surrounded by double quotes, quote characters in the return value will be
+escaped. This only applies if the quote character immediately precedes the
opening brace, and immediately follows the closing brace, with no whitespace.
-User callback functions are defined by passing a function object to the
+User callback functions are defined by passing a function object to the
template processor. Function names appearing in the text are first checked
against built-in function names, then against the method names in the function
-object. The function object forms a sandbox for execution of the template, so
+object. The function object forms a sandbox for execution of the template, so
security-conscious users may wish to avoid including functions that allow
arbitrary filesystem access or code execution.
-The callback function will receive its parameters as strings. If the
-result of the function depends only on the arguments, and certain things
+The callback function will receive its parameters as strings. If the
+result of the function depends only on the arguments, and certain things
understood to be "static", such as the source code, then the callback function
should return a string. If the result depends on other things, then the function
should call cbt_value() to get a return value:
return cbt_value( $text, $deps );
-where $deps is an array of string tokens, each one naming a dependency. As a
+where $deps is an array of string tokens, each one naming a dependency. As a
shortcut, if there is only one dependency, $deps may be a string.
diff --git a/includes/db/Database.php b/includes/db/Database.php
new file mode 100644
index 00000000..885ede54
--- /dev/null
+++ b/includes/db/Database.php
@@ -0,0 +1,2699 @@
+<?php
+/**
+ * @defgroup Database Database
+ *
+ * @file
+ * @ingroup Database
+ * This file deals with MySQL interface functions
+ * and query specifics/optimisations
+ */
+
+/** Number of times to re-try an operation in case of deadlock */
+define( 'DEADLOCK_TRIES', 4 );
+/** Minimum time to wait before retry, in microseconds */
+define( 'DEADLOCK_DELAY_MIN', 500000 );
+/** Maximum time to wait before retry */
+define( 'DEADLOCK_DELAY_MAX', 1500000 );
+
+/**
+ * Database abstraction object
+ * @ingroup Database
+ */
+class Database {
+
+#------------------------------------------------------------------------------
+# Variables
+#------------------------------------------------------------------------------
+
+ protected $mLastQuery = '';
+ protected $mPHPError = false;
+
+ protected $mServer, $mUser, $mPassword, $mConn = null, $mDBname;
+ protected $mOpened = false;
+
+ protected $mFailFunction;
+ protected $mTablePrefix;
+ protected $mFlags;
+ protected $mTrxLevel = 0;
+ protected $mErrorCount = 0;
+ protected $mLBInfo = array();
+ protected $mFakeSlaveLag = null, $mFakeMaster = false;
+
+#------------------------------------------------------------------------------
+# Accessors
+#------------------------------------------------------------------------------
+ # These optionally set a variable and return the previous state
+
+ /**
+ * Fail function, takes a Database as a parameter
+ * Set to false for default, 1 for ignore errors
+ */
+ function failFunction( $function = NULL ) {
+ return wfSetVar( $this->mFailFunction, $function );
+ }
+
+ /**
+ * Output page, used for reporting errors
+ * FALSE means discard output
+ */
+ function setOutputPage( $out ) {
+ wfDeprecated( __METHOD__ );
+ }
+
+ /**
+ * Boolean, controls output of large amounts of debug information
+ */
+ function debug( $debug = NULL ) {
+ return wfSetBit( $this->mFlags, DBO_DEBUG, $debug );
+ }
+
+ /**
+ * Turns buffering of SQL result sets on (true) or off (false).
+ * Default is "on" and it should not be changed without good reasons.
+ */
+ function bufferResults( $buffer = NULL ) {
+ if ( is_null( $buffer ) ) {
+ return !(bool)( $this->mFlags & DBO_NOBUFFER );
+ } else {
+ return !wfSetBit( $this->mFlags, DBO_NOBUFFER, !$buffer );
+ }
+ }
+
+ /**
+ * Turns on (false) or off (true) the automatic generation and sending
+ * of a "we're sorry, but there has been a database error" page on
+ * database errors. Default is on (false). When turned off, the
+ * code should use lastErrno() and lastError() to handle the
+ * situation as appropriate.
+ */
+ function ignoreErrors( $ignoreErrors = NULL ) {
+ return wfSetBit( $this->mFlags, DBO_IGNORE, $ignoreErrors );
+ }
+
+ /**
+ * The current depth of nested transactions
+ * @param $level Integer: , default NULL.
+ */
+ function trxLevel( $level = NULL ) {
+ return wfSetVar( $this->mTrxLevel, $level );
+ }
+
+ /**
+ * Number of errors logged, only useful when errors are ignored
+ */
+ function errorCount( $count = NULL ) {
+ return wfSetVar( $this->mErrorCount, $count );
+ }
+
+ function tablePrefix( $prefix = null ) {
+ return wfSetVar( $this->mTablePrefix, $prefix );
+ }
+
+ /**
+ * Properties passed down from the server info array of the load balancer
+ */
+ function getLBInfo( $name = NULL ) {
+ if ( is_null( $name ) ) {
+ return $this->mLBInfo;
+ } else {
+ if ( array_key_exists( $name, $this->mLBInfo ) ) {
+ return $this->mLBInfo[$name];
+ } else {
+ return NULL;
+ }
+ }
+ }
+
+ function setLBInfo( $name, $value = NULL ) {
+ if ( is_null( $value ) ) {
+ $this->mLBInfo = $name;
+ } else {
+ $this->mLBInfo[$name] = $value;
+ }
+ }
+
+ /**
+ * Set lag time in seconds for a fake slave
+ */
+ function setFakeSlaveLag( $lag ) {
+ $this->mFakeSlaveLag = $lag;
+ }
+
+ /**
+ * Make this connection a fake master
+ */
+ function setFakeMaster( $enabled = true ) {
+ $this->mFakeMaster = $enabled;
+ }
+
+ /**
+ * Returns true if this database supports (and uses) cascading deletes
+ */
+ function cascadingDeletes() {
+ return false;
+ }
+
+ /**
+ * Returns true if this database supports (and uses) triggers (e.g. on the page table)
+ */
+ function cleanupTriggers() {
+ return false;
+ }
+
+ /**
+ * Returns true if this database is strict about what can be put into an IP field.
+ * Specifically, it uses a NULL value instead of an empty string.
+ */
+ function strictIPs() {
+ return false;
+ }
+
+ /**
+ * Returns true if this database uses timestamps rather than integers
+ */
+ function realTimestamps() {
+ return false;
+ }
+
+ /**
+ * Returns true if this database does an implicit sort when doing GROUP BY
+ */
+ function implicitGroupby() {
+ return true;
+ }
+
+ /**
+ * Returns true if this database does an implicit order by when the column has an index
+ * For example: SELECT page_title FROM page LIMIT 1
+ */
+ function implicitOrderby() {
+ return true;
+ }
+
+ /**
+ * Returns true if this database can do a native search on IP columns
+ * e.g. this works as expected: .. WHERE rc_ip = '127.42.12.102/32';
+ */
+ function searchableIPs() {
+ return false;
+ }
+
+ /**
+ * Returns true if this database can use functional indexes
+ */
+ function functionalIndexes() {
+ return false;
+ }
+
+ /**#@+
+ * Get function
+ */
+ function lastQuery() { return $this->mLastQuery; }
+ function isOpen() { return $this->mOpened; }
+ /**#@-*/
+
+ function setFlag( $flag ) {
+ $this->mFlags |= $flag;
+ }
+
+ function clearFlag( $flag ) {
+ $this->mFlags &= ~$flag;
+ }
+
+ function getFlag( $flag ) {
+ return !!($this->mFlags & $flag);
+ }
+
+ /**
+ * General read-only accessor
+ */
+ function getProperty( $name ) {
+ return $this->$name;
+ }
+
+ function getWikiID() {
+ if( $this->mTablePrefix ) {
+ return "{$this->mDBname}-{$this->mTablePrefix}";
+ } else {
+ return $this->mDBname;
+ }
+ }
+
+#------------------------------------------------------------------------------
+# 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 $flags
+ * @param $tablePrefix String: database table prefixes. By default use the prefix gave in LocalSettings.php
+ */
+ function __construct( $server = false, $user = false, $password = false, $dbName = false,
+ $failFunction = false, $flags = 0, $tablePrefix = 'get from global' ) {
+
+ global $wgOut, $wgDBprefix, $wgCommandLineMode;
+ # Can't get a reference if it hasn't been set yet
+ if ( !isset( $wgOut ) ) {
+ $wgOut = NULL;
+ }
+
+ $this->mFailFunction = $failFunction;
+ $this->mFlags = $flags;
+
+ if ( $this->mFlags & DBO_DEFAULT ) {
+ if ( $wgCommandLineMode ) {
+ $this->mFlags &= ~DBO_TRX;
+ } else {
+ $this->mFlags |= DBO_TRX;
+ }
+ }
+
+ /*
+ // Faster read-only access
+ if ( wfReadOnly() ) {
+ $this->mFlags |= DBO_PERSISTENT;
+ $this->mFlags &= ~DBO_TRX;
+ }*/
+
+ /** Get the default table prefix*/
+ if ( $tablePrefix == 'get from global' ) {
+ $this->mTablePrefix = $wgDBprefix;
+ } else {
+ $this->mTablePrefix = $tablePrefix;
+ }
+
+ if ( $server ) {
+ $this->open( $server, $user, $password, $dbName );
+ }
+ }
+
+ /**
+ * @static
+ * @param failFunction
+ * @param $flags
+ */
+ static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0 )
+ {
+ return new Database( $server, $user, $password, $dbName, $failFunction, $flags );
+ }
+
+ /**
+ * Usually aborts on failure
+ * If the failFunction is set to a non-zero integer, returns success
+ */
+ function open( $server, $user, $password, $dbName ) {
+ global $wguname, $wgAllDBsAreLocalhost;
+ wfProfileIn( __METHOD__ );
+
+ # Test for missing mysql.so
+ # First try to load it
+ if (!@extension_loaded('mysql')) {
+ @dl('mysql.so');
+ }
+
+ # Fail now
+ # Otherwise we get a suppressed fatal error, which is very hard to track down
+ if ( !function_exists( 'mysql_connect' ) ) {
+ throw new DBConnectionError( $this, "MySQL functions missing, have you compiled PHP with the --with-mysql option?\n" );
+ }
+
+ # Debugging hack -- fake cluster
+ if ( $wgAllDBsAreLocalhost ) {
+ $realServer = 'localhost';
+ } else {
+ $realServer = $server;
+ }
+ $this->close();
+ $this->mServer = $server;
+ $this->mUser = $user;
+ $this->mPassword = $password;
+ $this->mDBname = $dbName;
+
+ $success = false;
+
+ 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.
+ $this->mConn = false;
+ $max = 3;
+ $this->installErrorHandler();
+ for ( $i = 0; $i < $max && !$this->mConn; $i++ ) {
+ if ( $i > 1 ) {
+ usleep( 1000 );
+ }
+ if ( $this->mFlags & DBO_PERSISTENT ) {
+ $this->mConn = mysql_pconnect( $realServer, $user, $password );
+ } else {
+ # Create a new connection...
+ $this->mConn = mysql_connect( $realServer, $user, $password, true );
+ }
+ if ($this->mConn === false) {
+ #$iplus = $i + 1;
+ #wfLogDBError("Connect loop error $iplus of $max ($server): " . mysql_errno() . " - " . mysql_error()."\n");
+ }
+ }
+ $phpError = $this->restoreErrorHandler();
+
+ 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;
+ }
+ } else {
+ # Delay USE query
+ $success = (bool)$this->mConn;
+ }
+
+ if ( $success ) {
+ $version = $this->getServerVersion();
+ if ( version_compare( $version, '4.1' ) >= 0 ) {
+ // Tell the server we're communicating with it in UTF-8.
+ // This may engage various charset conversions.
+ global $wgDBmysql5;
+ if( $wgDBmysql5 ) {
+ $this->query( 'SET NAMES utf8', __METHOD__ );
+ }
+ // Turn off strict mode
+ $this->query( "SET sql_mode = ''", __METHOD__ );
+ }
+
+ // Turn off strict mode if it is on
+ } else {
+ $this->reportConnectionError( $phpError );
+ }
+
+ $this->mOpened = $success;
+ wfProfileOut( __METHOD__ );
+ return $success;
+ }
+ /**@}}*/
+
+ protected function installErrorHandler() {
+ $this->mPHPError = false;
+ set_error_handler( array( $this, 'connectionErrorHandler' ) );
+ }
+
+ protected function restoreErrorHandler() {
+ restore_error_handler();
+ return $this->mPHPError;
+ }
+
+ protected function connectionErrorHandler( $errno, $errstr ) {
+ $this->mPHPError = $errstr;
+ }
+
+ /**
+ * Closes a database connection.
+ * if it is open : commits any open transactions
+ *
+ * @return bool operation success. true if already closed.
+ */
+ function close()
+ {
+ $this->mOpened = false;
+ if ( $this->mConn ) {
+ if ( $this->trxLevel() ) {
+ $this->immediateCommit();
+ }
+ return mysql_close( $this->mConn );
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * @param string $error fallback error message, used if none is given by MySQL
+ */
+ function reportConnectionError( $error = 'Unknown error' ) {
+ $myError = $this->lastError();
+ if ( $myError ) {
+ $error = $myError;
+ }
+
+ if ( $this->mFailFunction ) {
+ # Legacy error handling method
+ if ( !is_int( $this->mFailFunction ) ) {
+ $ff = $this->mFailFunction;
+ $ff( $this, $error );
+ }
+ } else {
+ # New method
+ wfLogDBError( "Connection error: $error\n" );
+ throw new DBConnectionError( $this, $error );
+ }
+ }
+
+ /**
+ * Usually aborts on failure. If errors are explicitly ignored, returns success.
+ *
+ * @param $sql String: SQL query
+ * @param $fname String: Name of the calling function, for profiling/SHOW PROCESSLIST
+ * comment (you can use __METHOD__ or add some extra info)
+ * @param $tempIgnore Bool: Whether to avoid throwing an exception on errors...
+ * maybe best to catch the exception instead?
+ * @return true for a successful write query, ResultWrapper object for a successful read query,
+ * or false on failure if $tempIgnore set
+ * @throws DBQueryError Thrown when the database returns an error of any kind
+ */
+ public function query( $sql, $fname = '', $tempIgnore = false ) {
+ global $wgProfiler;
+
+ $isMaster = !is_null( $this->getLBInfo( 'master' ) );
+ if ( isset( $wgProfiler ) ) {
+ # generalizeSQL will probably cut down the query to reasonable
+ # logging size most of the time. The substr is really just a sanity check.
+
+ # Who's been wasting my precious column space? -- TS
+ #$profName = 'query: ' . $fname . ' ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
+
+ if ( $isMaster ) {
+ $queryProf = 'query-m: ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
+ $totalProf = 'Database::query-master';
+ } else {
+ $queryProf = 'query: ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
+ $totalProf = 'Database::query';
+ }
+ wfProfileIn( $totalProf );
+ wfProfileIn( $queryProf );
+ }
+
+ $this->mLastQuery = $sql;
+
+ # Add a comment for easy SHOW PROCESSLIST interpretation
+ #if ( $fname ) {
+ global $wgUser;
+ if ( is_object( $wgUser ) && !($wgUser instanceof StubObject) ) {
+ $userName = $wgUser->getName();
+ if ( mb_strlen( $userName ) > 15 ) {
+ $userName = mb_substr( $userName, 0, 15 ) . '...';
+ }
+ $userName = str_replace( '/', '', $userName );
+ } else {
+ $userName = '';
+ }
+ $commentedSql = preg_replace('/\s/', " /* $fname $userName */ ", $sql, 1);
+ #} else {
+ # $commentedSql = $sql;
+ #}
+
+ # If DBO_TRX is set, start a transaction
+ if ( ( $this->mFlags & DBO_TRX ) && !$this->trxLevel() &&
+ $sql != 'BEGIN' && $sql != 'COMMIT' && $sql != 'ROLLBACK') {
+ // avoid establishing transactions for SHOW and SET statements too -
+ // that would delay transaction initializations to once connection
+ // is really used by application
+ $sqlstart = substr($sql,0,10); // very much worth it, benchmark certified(tm)
+ if (strpos($sqlstart,"SHOW ")!==0 and strpos($sqlstart,"SET ")!==0)
+ $this->begin();
+ }
+
+ if ( $this->debug() ) {
+ $sqlx = substr( $commentedSql, 0, 500 );
+ $sqlx = strtr( $sqlx, "\t\n", ' ' );
+ if ( $isMaster ) {
+ wfDebug( "SQL-master: $sqlx\n" );
+ } else {
+ wfDebug( "SQL: $sqlx\n" );
+ }
+ }
+
+ # Do the query and handle errors
+ $ret = $this->doQuery( $commentedSql );
+
+ # Try reconnecting if the connection was lost
+ if ( false === $ret && ( $this->lastErrno() == 2013 || $this->lastErrno() == 2006 ) ) {
+ # Transaction is gone, like it or not
+ $this->mTrxLevel = 0;
+ wfDebug( "Connection lost, reconnecting...\n" );
+ if ( $this->ping() ) {
+ wfDebug( "Reconnected\n" );
+ $sqlx = substr( $commentedSql, 0, 500 );
+ $sqlx = strtr( $sqlx, "\t\n", ' ' );
+ global $wgRequestTime;
+ $elapsed = round( microtime(true) - $wgRequestTime, 3 );
+ wfLogDBError( "Connection lost and reconnected after {$elapsed}s, query: $sqlx\n" );
+ $ret = $this->doQuery( $commentedSql );
+ } else {
+ wfDebug( "Failed\n" );
+ }
+ }
+
+ if ( false === $ret ) {
+ $this->reportQueryError( $this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore );
+ }
+
+ if ( isset( $wgProfiler ) ) {
+ wfProfileOut( $queryProf );
+ wfProfileOut( $totalProf );
+ }
+ return $this->resultObject( $ret );
+ }
+
+ /**
+ * 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*/ function doQuery( $sql ) {
+ if( $this->bufferResults() ) {
+ $ret = mysql_query( $sql, $this->mConn );
+ } else {
+ $ret = mysql_unbuffered_query( $sql, $this->mConn );
+ }
+ return $ret;
+ }
+
+ /**
+ * @param $error
+ * @param $errno
+ * @param $sql
+ * @param string $fname
+ * @param bool $tempIgnore
+ */
+ function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
+ global $wgCommandLineMode;
+ # Ignore errors during error handling to avoid infinite recursion
+ $ignore = $this->ignoreErrors( true );
+ ++$this->mErrorCount;
+
+ if( $ignore || $tempIgnore ) {
+ wfDebug("SQL ERROR (ignored): $error\n");
+ $this->ignoreErrors( $ignore );
+ } else {
+ $sql1line = str_replace( "\n", "\\n", $sql );
+ wfLogDBError("$fname\t{$this->mServer}\t$errno\t$error\t$sql1line\n");
+ wfDebug("SQL ERROR: " . $error . "\n");
+ throw new DBQueryError( $this, $error, $errno, $sql, $fname );
+ }
+ }
+
+
+ /**
+ * Intended to be compatible with the PEAR::DB wrapper functions.
+ * http://pear.php.net/manual/en/package.database.db.intro-execute.php
+ *
+ * ? = scalar value, quoted as necessary
+ * ! = raw SQL bit (a function for instance)
+ * & = filename; reads the file and inserts as a blob
+ * (we don't use this though...)
+ */
+ function prepare( $sql, $func = 'Database::prepare' ) {
+ /* MySQL doesn't support prepared statements (yet), so just
+ pack up the query for reference. We'll manually replace
+ the bits later. */
+ return array( 'query' => $sql, 'func' => $func );
+ }
+
+ function freePrepared( $prepared ) {
+ /* No-op for MySQL */
+ }
+
+ /**
+ * 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
+ */
+ function execute( $prepared, $args = null ) {
+ if( !is_array( $args ) ) {
+ # Pull the var args
+ $args = func_get_args();
+ array_shift( $args );
+ }
+ $sql = $this->fillPrepared( $prepared['query'], $args );
+ return $this->query( $sql, $prepared['func'] );
+ }
+
+ /**
+ * Prepare & execute an SQL statement, quoting and inserting arguments
+ * in the appropriate places.
+ * @param string $query
+ * @param string $args ...
+ */
+ function safeQuery( $query, $args = null ) {
+ $prepared = $this->prepare( $query, 'Database::safeQuery' );
+ if( !is_array( $args ) ) {
+ # Pull the var args
+ $args = func_get_args();
+ array_shift( $args );
+ }
+ $retval = $this->execute( $prepared, $args );
+ $this->freePrepared( $prepared );
+ return $retval;
+ }
+
+ /**
+ * 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
+ * @return string executable SQL
+ */
+ function fillPrepared( $preparedQuery, $args ) {
+ reset( $args );
+ $this->preparedArgs =& $args;
+ return preg_replace_callback( '/(\\\\[?!&]|[?!&])/',
+ array( &$this, 'fillPreparedArg' ), $preparedQuery );
+ }
+
+ /**
+ * preg_callback func for fillPrepared()
+ * The arguments should be in $this->preparedArgs and must not be touched
+ * while we're doing this.
+ *
+ * @param array $matches
+ * @return string
+ * @private
+ */
+ function fillPreparedArg( $matches ) {
+ switch( $matches[1] ) {
+ case '\\?': return '?';
+ case '\\!': return '!';
+ case '\\&': return '&';
+ }
+ list( /* $n */ , $arg ) = each( $this->preparedArgs );
+ switch( $matches[1] ) {
+ case '?': return $this->addQuotes( $arg );
+ case '!': return $arg;
+ case '&':
+ # return $this->addQuotes( file_get_contents( $arg ) );
+ throw new DBUnexpectedError( $this, '& mode is not implemented. If it\'s really needed, uncomment the line above.' );
+ default:
+ throw new DBUnexpectedError( $this, 'Received invalid match. This should never happen!' );
+ }
+ }
+
+ /**#@+
+ * @param mixed $res A SQL result
+ */
+ /**
+ * Free a result object
+ */
+ function freeResult( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ if ( !@/**/mysql_free_result( $res ) ) {
+ throw new DBUnexpectedError( $this, "Unable to free MySQL result" );
+ }
+ }
+
+ /**
+ * Fetch the next row from the given result object, in object form.
+ * Fields can be retrieved with $row->fieldname, with fields acting like
+ * member variables.
+ *
+ * @param $res SQL result object as returned from Database::query(), etc.
+ * @return MySQL row object
+ * @throws DBUnexpectedError Thrown if the database returns an error
+ */
+ function fetchObject( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ @/**/$row = mysql_fetch_object( $res );
+ if( $this->lastErrno() ) {
+ throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) );
+ }
+ return $row;
+ }
+
+ /**
+ * Fetch the next row from the given result object, in associative array
+ * form. Fields are retrieved with $row['fieldname'].
+ *
+ * @param $res SQL result object as returned from Database::query(), etc.
+ * @return MySQL row object
+ * @throws DBUnexpectedError Thrown if the database returns an error
+ */
+ function fetchRow( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ @/**/$row = mysql_fetch_array( $res );
+ if ( $this->lastErrno() ) {
+ throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) );
+ }
+ return $row;
+ }
+
+ /**
+ * Get the number of rows in a result object
+ */
+ function numRows( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ @/**/$n = mysql_num_rows( $res );
+ if( $this->lastErrno() ) {
+ throw new DBUnexpectedError( $this, 'Error in numRows(): ' . htmlspecialchars( $this->lastError() ) );
+ }
+ return $n;
+ }
+
+ /**
+ * Get the number of fields in a result object
+ * See documentation for mysql_num_fields()
+ */
+ function numFields( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return mysql_num_fields( $res );
+ }
+
+ /**
+ * Get a field name in a result object
+ * See documentation for mysql_field_name():
+ * http://www.php.net/mysql_field_name
+ */
+ function fieldName( $res, $n ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return mysql_field_name( $res, $n );
+ }
+
+ /**
+ * Get the inserted value of an auto-increment row
+ *
+ * The value inserted should be fetched from nextSequenceValue()
+ *
+ * Example:
+ * $id = $dbw->nextSequenceValue('page_page_id_seq');
+ * $dbw->insert('page',array('page_id' => $id));
+ * $id = $dbw->insertId();
+ */
+ function insertId() { return mysql_insert_id( $this->mConn ); }
+
+ /**
+ * Change the position of the cursor in a result object
+ * See mysql_data_seek()
+ */
+ function dataSeek( $res, $row ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return mysql_data_seek( $res, $row );
+ }
+
+ /**
+ * Get the last error number
+ * See mysql_errno()
+ */
+ function lastErrno() {
+ if ( $this->mConn ) {
+ return mysql_errno( $this->mConn );
+ } else {
+ return mysql_errno();
+ }
+ }
+
+ /**
+ * Get a description of the last error
+ * See mysql_error() for more details
+ */
+ function lastError() {
+ if ( $this->mConn ) {
+ # Even if it's non-zero, it can still be invalid
+ wfSuppressWarnings();
+ $error = mysql_error( $this->mConn );
+ if ( !$error ) {
+ $error = mysql_error();
+ }
+ wfRestoreWarnings();
+ } else {
+ $error = mysql_error();
+ }
+ if( $error ) {
+ $error .= ' (' . $this->mServer . ')';
+ }
+ return $error;
+ }
+ /**
+ * Get the number of rows affected by the last write query
+ * See mysql_affected_rows() for more details
+ */
+ function affectedRows() { return mysql_affected_rows( $this->mConn ); }
+ /**#@-*/ // end of template : @param $result
+
+ /**
+ * Simple UPDATE wrapper
+ * Usually aborts on failure
+ * If errors are explicitly ignored, returns success
+ *
+ * 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' )
+ {
+ $table = $this->tableName( $table );
+ $sql = "UPDATE $table SET $var = '" .
+ $this->strencode( $value ) . "' WHERE ($cond)";
+ return (bool)$this->query( $sql, $fname );
+ }
+
+ /**
+ * Simple SELECT wrapper, returns a single field, input must be encoded
+ * Usually aborts on failure
+ * If errors are explicitly ignored, returns FALSE on failure
+ */
+ function selectField( $table, $var, $cond='', $fname = 'Database::selectField', $options = array() ) {
+ if ( !is_array( $options ) ) {
+ $options = array( $options );
+ }
+ $options['LIMIT'] = 1;
+
+ $res = $this->select( $table, $var, $cond, $fname, $options );
+ if ( $res === false || !$this->numRows( $res ) ) {
+ return false;
+ }
+ $row = $this->fetchRow( $res );
+ if ( $row !== false ) {
+ $this->freeResult( $res );
+ return $row[0];
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Returns an optional USE INDEX clause to go after the table, and a
+ * string to go at the end of the query
+ *
+ * @private
+ *
+ * @param array $options an associative array of options to be turned into
+ * an SQL query, valid keys are listed in the function.
+ * @return array
+ */
+ function makeSelectOptions( $options ) {
+ $preLimitTail = $postLimitTail = '';
+ $startOpts = '';
+
+ $noKeyOptions = array();
+ foreach ( $options as $key => $option ) {
+ if ( is_numeric( $key ) ) {
+ $noKeyOptions[$option] = true;
+ }
+ }
+
+ if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY {$options['GROUP BY']}";
+ if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}";
+ if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY {$options['ORDER BY']}";
+
+ //if (isset($options['LIMIT'])) {
+ // $tailOpts .= $this->limitResult('', $options['LIMIT'],
+ // isset($options['OFFSET']) ? $options['OFFSET']
+ // : false);
+ //}
+
+ if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $postLimitTail .= ' FOR UPDATE';
+ if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $postLimitTail .= ' LOCK IN SHARE MODE';
+ if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
+
+ # Various MySQL extensions
+ if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) $startOpts .= ' /*! STRAIGHT_JOIN */';
+ if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) $startOpts .= ' HIGH_PRIORITY';
+ if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) $startOpts .= ' SQL_BIG_RESULT';
+ if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) $startOpts .= ' SQL_BUFFER_RESULT';
+ if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) $startOpts .= ' SQL_SMALL_RESULT';
+ if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) $startOpts .= ' SQL_CALC_FOUND_ROWS';
+ if ( isset( $noKeyOptions['SQL_CACHE'] ) ) $startOpts .= ' SQL_CACHE';
+ if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) $startOpts .= ' SQL_NO_CACHE';
+
+ if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) {
+ $useIndex = $this->useIndexClause( $options['USE INDEX'] );
+ } else {
+ $useIndex = '';
+ }
+
+ return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
+ }
+
+ /**
+ * 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') )
+ * @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() )
+ {
+ $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
+ return $this->query( $sql, $fname );
+ }
+
+ /**
+ * 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') )
+ * @return string, the SQL text
+ */
+ function selectSQLText( $table, $vars, $conds='', $fname = 'Database::select', $options = array(), $join_conds = array() ) {
+ if( is_array( $vars ) ) {
+ $vars = implode( ',', $vars );
+ }
+ if( !is_array( $options ) ) {
+ $options = array( $options );
+ }
+ if( is_array( $table ) ) {
+ if ( !empty($join_conds) || ( isset( $options['USE INDEX'] ) && is_array( @$options['USE INDEX'] ) ) )
+ $from = ' FROM ' . $this->tableNamesWithUseIndexOrJOIN( $table, @$options['USE INDEX'], $join_conds );
+ else
+ $from = ' FROM ' . implode( ',', array_map( array( &$this, 'tableName' ), $table ) );
+ } elseif ($table!='') {
+ if ($table{0}==' ') {
+ $from = ' FROM ' . $table;
+ } else {
+ $from = ' FROM ' . $this->tableName( $table );
+ }
+ } else {
+ $from = '';
+ }
+
+ list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) = $this->makeSelectOptions( $options );
+
+ if( !empty( $conds ) ) {
+ if ( is_array( $conds ) ) {
+ $conds = $this->makeList( $conds, LIST_AND );
+ }
+ $sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $preLimitTail";
+ } else {
+ $sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail";
+ }
+
+ if (isset($options['LIMIT']))
+ $sql = $this->limitResult($sql, $options['LIMIT'],
+ isset($options['OFFSET']) ? $options['OFFSET'] : false);
+ $sql = "$sql $postLimitTail";
+
+ if (isset($options['EXPLAIN'])) {
+ $sql = 'EXPLAIN ' . $sql;
+ }
+ return $sql;
+ }
+
+ /**
+ * Single row SELECT wrapper
+ * Aborts or returns FALSE on error
+ *
+ * $vars: the selected variables
+ * $conds: 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
+ *
+ * @todo migrate documentation to phpdocumentor format
+ */
+ function selectRow( $table, $vars, $conds, $fname = 'Database::selectRow', $options = array(), $join_conds = array() ) {
+ $options['LIMIT'] = 1;
+ $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
+ if ( $res === false )
+ return false;
+ if ( !$this->numRows($res) ) {
+ $this->freeResult($res);
+ return false;
+ }
+ $obj = $this->fetchObject( $res );
+ $this->freeResult( $res );
+ return $obj;
+
+ }
+
+ /**
+ * Estimate rows in dataset
+ * Returns estimated count, based on EXPLAIN output
+ * Takes same arguments as Database::select()
+ */
+
+ function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) {
+ $options['EXPLAIN']=true;
+ $res = $this->select ($table, $vars, $conds, $fname, $options );
+ if ( $res === false )
+ return false;
+ if (!$this->numRows($res)) {
+ $this->freeResult($res);
+ return 0;
+ }
+
+ $rows=1;
+
+ while( $plan = $this->fetchObject( $res ) ) {
+ $rows *= ($plan->rows > 0)?$plan->rows:1; // avoid resetting to zero
+ }
+
+ $this->freeResult($res);
+ return $rows;
+ }
+
+
+ /**
+ * Removes most variables from an SQL query and replaces them with X or N for numbers.
+ * It's only slightly flawed. Don't use for anything important.
+ *
+ * @param string $sql A SQL Query
+ * @static
+ */
+ static function generalizeSQL( $sql ) {
+ # This does the same as the regexp below would do, but in such a way
+ # as to avoid crashing php on some large strings.
+ # $sql = preg_replace ( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql);
+
+ $sql = str_replace ( "\\\\", '', $sql);
+ $sql = str_replace ( "\\'", '', $sql);
+ $sql = str_replace ( "\\\"", '', $sql);
+ $sql = preg_replace ("/'.*'/s", "'X'", $sql);
+ $sql = preg_replace ('/".*"/s', "'X'", $sql);
+
+ # All newlines, tabs, etc replaced by single space
+ $sql = preg_replace ( '/\s+/', ' ', $sql);
+
+ # All numbers => N
+ $sql = preg_replace ('/-?[0-9]+/s', 'N', $sql);
+
+ return $sql;
+ }
+
+ /**
+ * Determines whether a field exists in a table
+ * Usually aborts on failure
+ * If errors are explicitly ignored, returns NULL on failure
+ */
+ function fieldExists( $table, $field, $fname = 'Database::fieldExists' ) {
+ $table = $this->tableName( $table );
+ $res = $this->query( 'DESCRIBE '.$table, $fname );
+ if ( !$res ) {
+ return NULL;
+ }
+
+ $found = false;
+
+ while ( $row = $this->fetchObject( $res ) ) {
+ if ( $row->Field == $field ) {
+ $found = true;
+ break;
+ }
+ }
+ return $found;
+ }
+
+ /**
+ * Determines whether an index exists
+ * Usually aborts on failure
+ * If errors are explicitly ignored, returns NULL on failure
+ */
+ function indexExists( $table, $index, $fname = 'Database::indexExists' ) {
+ $info = $this->indexInfo( $table, $index, $fname );
+ if ( is_null( $info ) ) {
+ return NULL;
+ } else {
+ return $info !== false;
+ }
+ }
+
+
+ /**
+ * Get information about an index into an object
+ * Returns false if the index does not exist
+ */
+ function indexInfo( $table, $index, $fname = 'Database::indexInfo' ) {
+ # SHOW INDEX works in MySQL 3.23.58, but SHOW INDEXES does not.
+ # SHOW INDEX should work for 3.x and up:
+ # http://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html
+ $table = $this->tableName( $table );
+ $sql = 'SHOW INDEX FROM '.$table;
+ $res = $this->query( $sql, $fname );
+ if ( !$res ) {
+ return NULL;
+ }
+
+ $result = array();
+ while ( $row = $this->fetchObject( $res ) ) {
+ if ( $row->Key_name == $index ) {
+ $result[] = $row;
+ }
+ }
+ $this->freeResult($res);
+
+ return empty($result) ? false : $result;
+ }
+
+ /**
+ * Query whether a given table exists
+ */
+ function tableExists( $table ) {
+ $table = $this->tableName( $table );
+ $old = $this->ignoreErrors( true );
+ $res = $this->query( "SELECT 1 FROM $table LIMIT 1" );
+ $this->ignoreErrors( $old );
+ if( $res ) {
+ $this->freeResult( $res );
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * mysql_fetch_field() wrapper
+ * Returns false if the field doesn't exist
+ *
+ * @param $table
+ * @param $field
+ */
+ function fieldInfo( $table, $field ) {
+ $table = $this->tableName( $table );
+ $res = $this->query( "SELECT * FROM $table LIMIT 1" );
+ $n = mysql_num_fields( $res->result );
+ for( $i = 0; $i < $n; $i++ ) {
+ $meta = mysql_fetch_field( $res->result, $i );
+ if( $field == $meta->name ) {
+ return new MySQLField($meta);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * mysql_field_type() wrapper
+ */
+ function fieldType( $res, $index ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return mysql_field_type( $res, $index );
+ }
+
+ /**
+ * Determines if a given index is unique
+ */
+ function indexUnique( $table, $index ) {
+ $indexInfo = $this->indexInfo( $table, $index );
+ if ( !$indexInfo ) {
+ return NULL;
+ }
+ return !$indexInfo[0]->Non_unique;
+ }
+
+ /**
+ * INSERT wrapper, inserts an array into a table
+ *
+ * $a may be a single associative array, or an array of these with numeric keys, for
+ * multi-row insert.
+ *
+ * Usually aborts on failure
+ * If errors are explicitly ignored, returns success
+ */
+ function insert( $table, $a, $fname = 'Database::insert', $options = array() ) {
+ # No rows to insert, easy just return now
+ if ( !count( $a ) ) {
+ return true;
+ }
+
+ $table = $this->tableName( $table );
+ if ( !is_array( $options ) ) {
+ $options = array( $options );
+ }
+ if ( isset( $a[0] ) && is_array( $a[0] ) ) {
+ $multi = true;
+ $keys = array_keys( $a[0] );
+ } else {
+ $multi = false;
+ $keys = array_keys( $a );
+ }
+
+ $sql = 'INSERT ' . implode( ' ', $options ) .
+ " INTO $table (" . implode( ',', $keys ) . ') VALUES ';
+
+ if ( $multi ) {
+ $first = true;
+ foreach ( $a as $row ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $sql .= ',';
+ }
+ $sql .= '(' . $this->makeList( $row ) . ')';
+ }
+ } else {
+ $sql .= '(' . $this->makeList( $a ) . ')';
+ }
+ return (bool)$this->query( $sql, $fname );
+ }
+
+ /**
+ * Make UPDATE options for the Database::update function
+ *
+ * @private
+ * @param array $options The options passed to Database::update
+ * @return string
+ */
+ function makeUpdateOptions( $options ) {
+ if( !is_array( $options ) ) {
+ $options = array( $options );
+ }
+ $opts = array();
+ if ( in_array( 'LOW_PRIORITY', $options ) )
+ $opts[] = $this->lowPriorityOption();
+ if ( in_array( 'IGNORE', $options ) )
+ $opts[] = 'IGNORE';
+ return implode(' ', $opts);
+ }
+
+ /**
+ * 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
+ * more of IGNORE, LOW_PRIORITY
+ * @return bool
+ */
+ function update( $table, $values, $conds, $fname = 'Database::update', $options = array() ) {
+ $table = $this->tableName( $table );
+ $opts = $this->makeUpdateOptions( $options );
+ $sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET );
+ if ( $conds != '*' ) {
+ $sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
+ }
+ return $this->query( $sql, $fname );
+ }
+
+ /**
+ * Makes an encoded list of strings from an array
+ * $mode:
+ * LIST_COMMA - comma separated, no field names
+ * LIST_AND - ANDed WHERE clause (without the WHERE)
+ * LIST_OR - ORed WHERE clause (without the WHERE)
+ * LIST_SET - comma separated with field names, like a SET clause
+ * LIST_NAMES - comma separated field names
+ */
+ function makeList( $a, $mode = LIST_COMMA ) {
+ if ( !is_array( $a ) ) {
+ throw new DBUnexpectedError( $this, 'Database::makeList called with incorrect parameters' );
+ }
+
+ $first = true;
+ $list = '';
+ foreach ( $a as $field => $value ) {
+ if ( !$first ) {
+ if ( $mode == LIST_AND ) {
+ $list .= ' AND ';
+ } elseif($mode == LIST_OR) {
+ $list .= ' OR ';
+ } else {
+ $list .= ',';
+ }
+ } else {
+ $first = false;
+ }
+ if ( ($mode == LIST_AND || $mode == LIST_OR) && is_numeric( $field ) ) {
+ $list .= "($value)";
+ } elseif ( ($mode == LIST_SET) && is_numeric( $field ) ) {
+ $list .= "$value";
+ } elseif ( ($mode == LIST_AND || $mode == LIST_OR) && is_array($value) ) {
+ if( count( $value ) == 0 ) {
+ throw new MWException( __METHOD__.': empty input' );
+ } elseif( count( $value ) == 1 ) {
+ // Special-case single values, as IN isn't terribly efficient
+ // Don't necessarily assume the single key is 0; we don't
+ // enforce linear numeric ordering on other arrays here.
+ $value = array_values( $value );
+ $list .= $field." = ".$this->addQuotes( $value[0] );
+ } else {
+ $list .= $field." IN (".$this->makeList($value).") ";
+ }
+ } elseif( is_null($value) ) {
+ if ( $mode == LIST_AND || $mode == LIST_OR ) {
+ $list .= "$field IS ";
+ } elseif ( $mode == LIST_SET ) {
+ $list .= "$field = ";
+ }
+ $list .= 'NULL';
+ } else {
+ if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) {
+ $list .= "$field = ";
+ }
+ $list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value );
+ }
+ }
+ return $list;
+ }
+
+ /**
+ * Change the current database
+ */
+ function selectDB( $db ) {
+ $this->mDBname = $db;
+ return mysql_select_db( $db, $this->mConn );
+ }
+
+ /**
+ * Get the current DB name
+ */
+ function getDBname() {
+ return $this->mDBname;
+ }
+
+ /**
+ * Get the server hostname or IP address
+ */
+ function getServer() {
+ return $this->mServer;
+ }
+
+ /**
+ * Format a table name ready for use in constructing an SQL query
+ *
+ * This does two important things: it quotes the table names to clean them up,
+ * and it adds a table prefix if only given a table name with no quotes.
+ *
+ * All functions of this object which require a table name call this function
+ * 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
+ */
+ function tableName( $name ) {
+ global $wgSharedDB, $wgSharedPrefix, $wgSharedTables;
+ # Skip the entire process when we have a string quoted on both ends.
+ # Note that we check the end so that we will still quote any use of
+ # use of `database`.table. But won't break things if someone wants
+ # to query a database table with a dot in the name.
+ if ( $name[0] == '`' && substr( $name, -1, 1 ) == '`' ) return $name;
+
+ # Lets test for any bits of text that should never show up in a table
+ # name. Basically anything like JOIN or ON which are actually part of
+ # SQL queries, but may end up inside of the table value to combine
+ # sql. Such as how the API is doing.
+ # Note that we use a whitespace test rather than a \b test to avoid
+ # any remote case where a word like on may be inside of a table name
+ # surrounded by symbols which may be considered word breaks.
+ if( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) return $name;
+
+ # Split database and table into proper variables.
+ # We reverse the explode so that database.table and table both output
+ # the correct table.
+ $dbDetails = array_reverse( explode( '.', $name, 2 ) );
+ if( isset( $dbDetails[1] ) ) @list( $table, $database ) = $dbDetails;
+ else @list( $table ) = $dbDetails;
+ $prefix = $this->mTablePrefix; # Default prefix
+
+ # A database name has been specified in input. Quote the table name
+ # because we don't want any prefixes added.
+ if( isset($database) ) $table = ( $table[0] == '`' ? $table : "`{$table}`" );
+
+ # Note that we use the long format because php will complain in in_array if
+ # the input is not an array, and will complain in is_array if it is not set.
+ if( !isset( $database ) # Don't use shared database if pre selected.
+ && isset( $wgSharedDB ) # We have a shared database
+ && $table[0] != '`' # Paranoia check to prevent shared tables listing '`table`'
+ && isset( $wgSharedTables )
+ && is_array( $wgSharedTables )
+ && in_array( $table, $wgSharedTables ) ) { # A shared table is selected
+ $database = $wgSharedDB;
+ $prefix = isset( $wgSharedPrefix ) ? $wgSharedPrefix : $prefix;
+ }
+
+ # Quote the $database and $table and apply the prefix if not quoted.
+ if( isset($database) ) $database = ( $database[0] == '`' ? $database : "`{$database}`" );
+ $table = ( $table[0] == '`' ? $table : "`{$prefix}{$table}`" );
+
+ # Merge our database and table into our final table name.
+ $tableName = ( isset($database) ? "{$database}.{$table}" : "{$table}" );
+
+ # We're finished, return.
+ return $tableName;
+ }
+
+ /**
+ * Fetch a number of table names into an array
+ * This is handy when you need to construct SQL for joins
+ *
+ * Example:
+ * extract($dbr->tableNames('user','watchlist'));
+ * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
+ * WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
+ */
+ public function tableNames() {
+ $inArray = func_get_args();
+ $retVal = array();
+ foreach ( $inArray as $name ) {
+ $retVal[$name] = $this->tableName( $name );
+ }
+ return $retVal;
+ }
+
+ /**
+ * Fetch a number of table names into an zero-indexed numerical array
+ * This is handy when you need to construct SQL for joins
+ *
+ * Example:
+ * list( $user, $watchlist ) = $dbr->tableNamesN('user','watchlist');
+ * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
+ * WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
+ */
+ public function tableNamesN() {
+ $inArray = func_get_args();
+ $retVal = array();
+ foreach ( $inArray as $name ) {
+ $retVal[] = $this->tableName( $name );
+ }
+ return $retVal;
+ }
+
+ /**
+ * @private
+ */
+ function tableNamesWithUseIndexOrJOIN( $tables, $use_index = array(), $join_conds = array() ) {
+ $ret = array();
+ $retJOIN = array();
+ $use_index_safe = is_array($use_index) ? $use_index : array();
+ $join_conds_safe = is_array($join_conds) ? $join_conds : array();
+ foreach ( $tables as $table ) {
+ // Is there a JOIN and INDEX clause for this table?
+ if ( isset($join_conds_safe[$table]) && isset($use_index_safe[$table]) ) {
+ $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
+ $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
+ $tableClause .= ' ON (' . $this->makeList((array)$join_conds_safe[$table][1], LIST_AND) . ')';
+ $retJOIN[] = $tableClause;
+ // Is there an INDEX clause?
+ } else if ( isset($use_index_safe[$table]) ) {
+ $tableClause = $this->tableName( $table );
+ $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
+ $ret[] = $tableClause;
+ // Is there a JOIN clause?
+ } else if ( isset($join_conds_safe[$table]) ) {
+ $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
+ $tableClause .= ' ON (' . $this->makeList((array)$join_conds_safe[$table][1], LIST_AND) . ')';
+ $retJOIN[] = $tableClause;
+ } else {
+ $tableClause = $this->tableName( $table );
+ $ret[] = $tableClause;
+ }
+ }
+ // We can't separate explicit JOIN clauses with ',', use ' ' for those
+ $straightJoins = !empty($ret) ? implode( ',', $ret ) : "";
+ $otherJoins = !empty($retJOIN) ? implode( ' ', $retJOIN ) : "";
+ // Compile our final table clause
+ return implode(' ',array($straightJoins,$otherJoins) );
+ }
+
+ /**
+ * Wrapper for addslashes()
+ * @param string $s String to be slashed.
+ * @return string slashed string.
+ */
+ function strencode( $s ) {
+ return mysql_real_escape_string( $s, $this->mConn );
+ }
+
+ /**
+ * If it's a string, adds quotes and backslashes
+ * Otherwise returns as-is
+ */
+ function addQuotes( $s ) {
+ if ( is_null( $s ) ) {
+ return 'NULL';
+ } else {
+ # This will also quote numeric values. This should be harmless,
+ # and protects against weird problems that occur when they really
+ # _are_ strings such as article titles and string->number->string
+ # conversion is not 1:1.
+ return "'" . $this->strencode( $s ) . "'";
+ }
+ }
+
+ /**
+ * Escape string for safe LIKE usage
+ */
+ function escapeLike( $s ) {
+ $s=$this->strencode( $s );
+ $s=str_replace(array('%','_'),array('\%','\_'),$s);
+ return $s;
+ }
+
+ /**
+ * Returns an appropriately quoted sequence value for inserting a new row.
+ * MySQL has autoincrement fields, so this is just NULL. But the PostgreSQL
+ * subclass will return an integer, and save the value for insertId()
+ */
+ function nextSequenceValue( $seqName ) {
+ return NULL;
+ }
+
+ /**
+ * USE INDEX clause
+ * PostgreSQL doesn't have them and returns ""
+ */
+ function useIndexClause( $index ) {
+ return "FORCE INDEX ($index)";
+ }
+
+ /**
+ * REPLACE query wrapper
+ * PostgreSQL simulates this with a DELETE followed by INSERT
+ * $row is the row to insert, an associative array
+ * $uniqueIndexes is an array of indexes. Each element may be either a
+ * field name or an array of field names
+ *
+ * It may be more efficient to leave off unique indexes which are unlikely to collide.
+ * However if you do this, you run the risk of encountering errors which wouldn't have
+ * occurred in MySQL
+ *
+ * @todo migrate comment to phodocumentor format
+ */
+ function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) {
+ $table = $this->tableName( $table );
+
+ # Single row case
+ if ( !is_array( reset( $rows ) ) ) {
+ $rows = array( $rows );
+ }
+
+ $sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) .') VALUES ';
+ $first = true;
+ foreach ( $rows as $row ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $sql .= ',';
+ }
+ $sql .= '(' . $this->makeList( $row ) . ')';
+ }
+ return $this->query( $sql, $fname );
+ }
+
+ /**
+ * DELETE where the condition is a join
+ * MySQL does this with a multi-table DELETE syntax, PostgreSQL does it with sub-selects
+ *
+ * For safety, an empty $conds will not delete everything. If you want to delete all rows where the
+ * join condition matches, set $conds='*'
+ *
+ * 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
+ */
+ function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'Database::deleteJoin' ) {
+ if ( !$conds ) {
+ throw new DBUnexpectedError( $this, 'Database::deleteJoin() called with empty $conds' );
+ }
+
+ $delTable = $this->tableName( $delTable );
+ $joinTable = $this->tableName( $joinTable );
+ $sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar ";
+ if ( $conds != '*' ) {
+ $sql .= ' AND ' . $this->makeList( $conds, LIST_AND );
+ }
+
+ return $this->query( $sql, $fname );
+ }
+
+ /**
+ * Returns the size of a text field, or -1 for "unlimited"
+ */
+ function textFieldSize( $table, $field ) {
+ $table = $this->tableName( $table );
+ $sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
+ $res = $this->query( $sql, 'Database::textFieldSize' );
+ $row = $this->fetchObject( $res );
+ $this->freeResult( $res );
+
+ $m = array();
+ if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
+ $size = $m[1];
+ } else {
+ $size = -1;
+ }
+ return $size;
+ }
+
+ /**
+ * @return string Returns the text of the low priority option if it is supported, or a blank string otherwise
+ */
+ function lowPriorityOption() {
+ return 'LOW_PRIORITY';
+ }
+
+ /**
+ * DELETE query wrapper
+ *
+ * Use $conds == "*" to delete all rows
+ */
+ function delete( $table, $conds, $fname = 'Database::delete' ) {
+ if ( !$conds ) {
+ throw new DBUnexpectedError( $this, 'Database::delete() called with no conditions' );
+ }
+ $table = $this->tableName( $table );
+ $sql = "DELETE FROM $table";
+ if ( $conds != '*' ) {
+ $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
+ }
+ return $this->query( $sql, $fname );
+ }
+
+ /**
+ * INSERT SELECT wrapper
+ * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...)
+ * Source items may be literals rather than field names, but strings should be quoted with Database::addQuotes()
+ * $conds may be "*" to copy the whole table
+ * srcTable may be an array of tables.
+ */
+ function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'Database::insertSelect',
+ $insertOptions = array(), $selectOptions = array() )
+ {
+ $destTable = $this->tableName( $destTable );
+ if ( is_array( $insertOptions ) ) {
+ $insertOptions = implode( ' ', $insertOptions );
+ }
+ if( !is_array( $selectOptions ) ) {
+ $selectOptions = array( $selectOptions );
+ }
+ list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
+ if( is_array( $srcTable ) ) {
+ $srcTable = implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) );
+ } else {
+ $srcTable = $this->tableName( $srcTable );
+ }
+ $sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
+ " SELECT $startOpts " . implode( ',', $varMap ) .
+ " FROM $srcTable $useIndex ";
+ if ( $conds != '*' ) {
+ $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
+ }
+ $sql .= " $tailOpts";
+ return $this->query( $sql, $fname );
+ }
+
+ /**
+ * 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)
+ */
+ function limitResult($sql, $limit, $offset=false) {
+ if( !is_numeric($limit) ) {
+ throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
+ }
+ return "$sql LIMIT "
+ . ( (is_numeric($offset) && $offset != 0) ? "{$offset}," : "" )
+ . "{$limit} ";
+ }
+ function limitResultForUpdate($sql, $num) {
+ return $this->limitResult($sql, $num, 0);
+ }
+
+ /**
+ * 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
+ */
+ function conditional( $cond, $trueVal, $falseVal ) {
+ return " IF($cond, $trueVal, $falseVal) ";
+ }
+
+ /**
+ * 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
+ */
+ function strreplace( $orig, $old, $new ) {
+ return "REPLACE({$orig}, {$old}, {$new})";
+ }
+
+ /**
+ * Determines if the last failure was due to a deadlock
+ */
+ function wasDeadlock() {
+ return $this->lastErrno() == 1213;
+ }
+
+ /**
+ * Perform a deadlock-prone transaction.
+ *
+ * This function invokes a callback function to perform a set of write
+ * queries. If a deadlock occurs during the processing, the transaction
+ * will be rolled back and the callback function will be called again.
+ *
+ * Usage:
+ * $dbw->deadlockLoop( callback, ... );
+ *
+ * Extra arguments are passed through to the specified callback function.
+ *
+ * Returns whatever the callback function returned on its successful,
+ * iteration, or false on error, for example if the retry limit was
+ * reached.
+ */
+ function deadlockLoop() {
+ $myFname = 'Database::deadlockLoop';
+
+ $this->begin();
+ $args = func_get_args();
+ $function = array_shift( $args );
+ $oldIgnore = $this->ignoreErrors( true );
+ $tries = DEADLOCK_TRIES;
+ if ( is_array( $function ) ) {
+ $fname = $function[0];
+ } else {
+ $fname = $function;
+ }
+ do {
+ $retVal = call_user_func_array( $function, $args );
+ $error = $this->lastError();
+ $errno = $this->lastErrno();
+ $sql = $this->lastQuery();
+
+ if ( $errno ) {
+ if ( $this->wasDeadlock() ) {
+ # Retry
+ usleep( mt_rand( DEADLOCK_DELAY_MIN, DEADLOCK_DELAY_MAX ) );
+ } else {
+ $this->reportQueryError( $error, $errno, $sql, $fname );
+ }
+ }
+ } while( $this->wasDeadlock() && --$tries > 0 );
+ $this->ignoreErrors( $oldIgnore );
+ if ( $tries <= 0 ) {
+ $this->query( 'ROLLBACK', $myFname );
+ $this->reportQueryError( $error, $errno, $sql, $fname );
+ return false;
+ } else {
+ $this->query( 'COMMIT', $myFname );
+ return $retVal;
+ }
+ }
+
+ /**
+ * 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
+ */
+ function masterPosWait( MySQLMasterPos $pos, $timeout ) {
+ $fname = 'Database::masterPosWait';
+ wfProfileIn( $fname );
+
+ # Commit any open transactions
+ if ( $this->mTrxLevel ) {
+ $this->immediateCommit();
+ }
+
+ if ( !is_null( $this->mFakeSlaveLag ) ) {
+ $wait = intval( ( $pos->pos - microtime(true) + $this->mFakeSlaveLag ) * 1e6 );
+ if ( $wait > $timeout * 1e6 ) {
+ wfDebug( "Fake slave timed out waiting for $pos ($wait us)\n" );
+ wfProfileOut( $fname );
+ return -1;
+ } elseif ( $wait > 0 ) {
+ wfDebug( "Fake slave waiting $wait us\n" );
+ usleep( $wait );
+ wfProfileOut( $fname );
+ return 1;
+ } else {
+ wfDebug( "Fake slave up to date ($wait us)\n" );
+ wfProfileOut( $fname );
+ return 0;
+ }
+ }
+
+ # Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set
+ $encFile = $this->addQuotes( $pos->file );
+ $encPos = intval( $pos->pos );
+ $sql = "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)";
+ $res = $this->doQuery( $sql );
+ if ( $res && $row = $this->fetchRow( $res ) ) {
+ $this->freeResult( $res );
+ wfProfileOut( $fname );
+ return $row[0];
+ } else {
+ wfProfileOut( $fname );
+ return false;
+ }
+ }
+
+ /**
+ * Get the position of the master from SHOW SLAVE STATUS
+ */
+ function getSlavePos() {
+ if ( !is_null( $this->mFakeSlaveLag ) ) {
+ $pos = new MySQLMasterPos( 'fake', microtime(true) - $this->mFakeSlaveLag );
+ wfDebug( __METHOD__.": fake slave pos = $pos\n" );
+ return $pos;
+ }
+ $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 );
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Get the position of the master from SHOW MASTER STATUS
+ */
+ function getMasterPos() {
+ if ( $this->mFakeMaster ) {
+ return new MySQLMasterPos( 'fake', microtime( true ) );
+ }
+ $res = $this->query( 'SHOW MASTER STATUS', 'Database::getMasterPos' );
+ $row = $this->fetchObject( $res );
+ if ( $row ) {
+ return new MySQLMasterPos( $row->File, $row->Position );
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Begin a transaction, committing any previously open transaction
+ */
+ function begin( $fname = 'Database::begin' ) {
+ $this->query( 'BEGIN', $fname );
+ $this->mTrxLevel = 1;
+ }
+
+ /**
+ * End a transaction
+ */
+ function commit( $fname = 'Database::commit' ) {
+ $this->query( 'COMMIT', $fname );
+ $this->mTrxLevel = 0;
+ }
+
+ /**
+ * Rollback a transaction.
+ * No-op on non-transactional databases.
+ */
+ function rollback( $fname = 'Database::rollback' ) {
+ $this->query( 'ROLLBACK', $fname, true );
+ $this->mTrxLevel = 0;
+ }
+
+ /**
+ * Begin a transaction, committing any previously open transaction
+ * @deprecated use begin()
+ */
+ function immediateBegin( $fname = 'Database::immediateBegin' ) {
+ $this->begin();
+ }
+
+ /**
+ * Commit transaction, if one is open
+ * @deprecated use commit()
+ */
+ function immediateCommit( $fname = 'Database::immediateCommit' ) {
+ $this->commit();
+ }
+
+ /**
+ * Return MW-style timestamp used for MySQL schema
+ */
+ function timestamp( $ts=0 ) {
+ return wfTimestamp(TS_MW,$ts);
+ }
+
+ /**
+ * Local database timestamp format or null
+ */
+ function timestampOrNull( $ts = null ) {
+ if( is_null( $ts ) ) {
+ return null;
+ } else {
+ return $this->timestamp( $ts );
+ }
+ }
+
+ /**
+ * @todo document
+ */
+ function resultObject( $result ) {
+ if( empty( $result ) ) {
+ return false;
+ } elseif ( $result instanceof ResultWrapper ) {
+ return $result;
+ } elseif ( $result === true ) {
+ // Successful write query
+ return $result;
+ } else {
+ return new ResultWrapper( $this, $result );
+ }
+ }
+
+ /**
+ * Return aggregated value alias
+ */
+ function aggregateValue ($valuedata,$valuename='value') {
+ return $valuename;
+ }
+
+ /**
+ * @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
+ */
+ function getServerVersion() {
+ return mysql_get_server_info( $this->mConn );
+ }
+
+ /**
+ * Ping the server and try to reconnect if it there is no connection
+ */
+ function ping() {
+ if( !function_exists( 'mysql_ping' ) ) {
+ wfDebug( "Tried to call mysql_ping but this is ancient PHP version. Faking it!\n" );
+ return true;
+ }
+ $ping = mysql_ping( $this->mConn );
+ if ( $ping ) {
+ return true;
+ }
+
+ // Need to reconnect manually in MySQL client 5.0.13+
+ if ( version_compare( mysql_get_client_info(), '5.0.13', '>=' ) ) {
+ mysql_close( $this->mConn );
+ $this->mOpened = false;
+ $this->mConn = false;
+ $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Get slave lag.
+ * At the moment, this will only work if the DB user has the PROCESS privilege
+ */
+ function getLag() {
+ if ( !is_null( $this->mFakeSlaveLag ) ) {
+ wfDebug( "getLag: fake slave lagged {$this->mFakeSlaveLag} seconds\n" );
+ return $this->mFakeSlaveLag;
+ }
+ $res = $this->query( 'SHOW PROCESSLIST' );
+ # Find slave SQL thread
+ while ( $row = $this->fetchObject( $res ) ) {
+ /* This should work for most situations - when default db
+ * for thread is not specified, it had no events executed,
+ * and therefore it doesn't know yet how lagged it is.
+ *
+ * Relay log I/O thread does not select databases.
+ */
+ if ( $row->User == 'system user' &&
+ $row->State != 'Waiting for master to send event' &&
+ $row->State != 'Connecting to master' &&
+ $row->State != 'Queueing master event to the relay log' &&
+ $row->State != 'Waiting for master update' &&
+ $row->State != 'Requesting binlog dump'
+ ) {
+ # This is it, return the time (except -ve)
+ if ( $row->Time > 0x7fffffff ) {
+ return false;
+ } else {
+ return $row->Time;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Get status information from SHOW STATUS in an associative array
+ */
+ function getStatus($which="%") {
+ $res = $this->query( "SHOW STATUS LIKE '{$which}'" );
+ $status = array();
+ while ( $row = $this->fetchObject( $res ) ) {
+ $status[$row->Variable_name] = $row->Value;
+ }
+ return $status;
+ }
+
+ /**
+ * Return the maximum number of items allowed in a list, or 0 for unlimited.
+ */
+ function maxListLen() {
+ return 0;
+ }
+
+ function encodeBlob($b) {
+ return $b;
+ }
+
+ function decodeBlob($b) {
+ return $b;
+ }
+
+ /**
+ * Override database's default connection timeout.
+ * May be useful for very long batch queries such as
+ * full-wiki dumps, where a single query reads out
+ * over hours or days.
+ * @param int $timeout in seconds
+ */
+ public function setTimeout( $timeout ) {
+ $this->query( "SET net_read_timeout=$timeout" );
+ $this->query( "SET net_write_timeout=$timeout" );
+ }
+
+ /**
+ * Read and execute SQL commands from a file.
+ * Returns true on success, error string 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
+ */
+ function sourceFile( $filename, $lineCallback = false, $resultCallback = false ) {
+ $fp = fopen( $filename, 'r' );
+ if ( false === $fp ) {
+ throw new MWException( "Could not open \"{$filename}\".\n" );
+ }
+ $error = $this->sourceStream( $fp, $lineCallback, $resultCallback );
+ fclose( $fp );
+ return $error;
+ }
+
+ /**
+ * Read and execute commands from an open file handle
+ * Returns true on success, error string 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
+ */
+ function sourceStream( $fp, $lineCallback = false, $resultCallback = false ) {
+ $cmd = "";
+ $done = false;
+ $dollarquote = false;
+
+ while ( ! feof( $fp ) ) {
+ if ( $lineCallback ) {
+ call_user_func( $lineCallback );
+ }
+ $line = trim( fgets( $fp, 1024 ) );
+ $sl = strlen( $line ) - 1;
+
+ if ( $sl < 0 ) { continue; }
+ if ( '-' == $line{0} && '-' == $line{1} ) { continue; }
+
+ ## Allow dollar quoting for function declarations
+ if (substr($line,0,4) == '$mw$') {
+ if ($dollarquote) {
+ $dollarquote = false;
+ $done = true;
+ }
+ else {
+ $dollarquote = true;
+ }
+ }
+ else if (!$dollarquote) {
+ if ( ';' == $line{$sl} && ($sl < 2 || ';' != $line{$sl - 1})) {
+ $done = true;
+ $line = substr( $line, 0, $sl );
+ }
+ }
+
+ if ( '' != $cmd ) { $cmd .= ' '; }
+ $cmd .= "$line\n";
+
+ if ( $done ) {
+ $cmd = str_replace(';;', ";", $cmd);
+ $cmd = $this->replaceVars( $cmd );
+ $res = $this->query( $cmd, __METHOD__ );
+ if ( $resultCallback ) {
+ call_user_func( $resultCallback, $res );
+ }
+
+ if ( false === $res ) {
+ $err = $this->lastError();
+ return "Query \"{$cmd}\" failed with error code \"$err\".\n";
+ }
+
+ $cmd = '';
+ $done = false;
+ }
+ }
+ return true;
+ }
+
+
+ /**
+ * Replace variables in sourced SQL
+ */
+ protected function replaceVars( $ins ) {
+ $varnames = array(
+ 'wgDBserver', 'wgDBname', 'wgDBintlname', 'wgDBuser',
+ 'wgDBpassword', 'wgDBsqluser', 'wgDBsqlpassword',
+ 'wgDBadminuser', 'wgDBadminpassword', 'wgDBTableOptions',
+ );
+
+ // Ordinary variables
+ foreach ( $varnames as $var ) {
+ if( isset( $GLOBALS[$var] ) ) {
+ $val = addslashes( $GLOBALS[$var] ); // FIXME: safety check?
+ $ins = str_replace( '{$' . $var . '}', $val, $ins );
+ $ins = str_replace( '/*$' . $var . '*/`', '`' . $val, $ins );
+ $ins = str_replace( '/*$' . $var . '*/', $val, $ins );
+ }
+ }
+
+ // Table prefixes
+ $ins = preg_replace_callback( '/\/\*(?:\$wgDBprefix|_)\*\/([a-zA-Z_0-9]*)/',
+ array( &$this, 'tableNameCallback' ), $ins );
+ return $ins;
+ }
+
+ /**
+ * Table name callback
+ * @private
+ */
+ protected function tableNameCallback( $matches ) {
+ return $this->tableName( $matches[1] );
+ }
+
+ /*
+ * Build a concatenation list to feed into a SQL query
+ */
+ function buildConcat( $stringList ) {
+ return 'CONCAT(' . implode( ',', $stringList ) . ')';
+ }
+
+ /**
+ * Acquire a lock
+ *
+ * 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
+ * @return bool
+ */
+ public function lock( $lockName, $method ) {
+ $lockName = $this->addQuotes( $lockName );
+ $result = $this->query( "SELECT GET_LOCK($lockName, 5) AS lockstatus", $method );
+ $row = $this->fetchObject( $result );
+ $this->freeResult( $result );
+
+ if( $row->lockstatus == 1 ) {
+ return true;
+ } else {
+ wfDebug( __METHOD__." failed to acquire lock\n" );
+ return false;
+ }
+ }
+ /**
+ * Release a lock.
+ *
+ * @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
+ */
+ public function unlock( $lockName, $method ) {
+ $lockName = $this->addQuotes( $lockName );
+ $result = $this->query( "SELECT RELEASE_LOCK($lockName)", $method );
+ $this->freeResult( $result );
+ }
+}
+
+/**
+ * Database abstraction object for mySQL
+ * Inherit all methods and properties of Database::Database()
+ *
+ * @ingroup Database
+ * @see Database
+ */
+class DatabaseMysql extends Database {
+ # Inherit all
+}
+
+/******************************************************************************
+ * Utility classes
+ *****************************************************************************/
+
+/**
+ * Utility class.
+ * @ingroup Database
+ */
+class DBObject {
+ public $mData;
+
+ function DBObject($data) {
+ $this->mData = $data;
+ }
+
+ function isLOB() {
+ return false;
+ }
+
+ function data() {
+ return $this->mData;
+ }
+}
+
+/**
+ * Utility class
+ * @ingroup Database
+ *
+ * This allows us to distinguish a blob from a normal string and an array of strings
+ */
+class Blob {
+ private $mData;
+ function __construct($data) {
+ $this->mData = $data;
+ }
+ function fetch() {
+ return $this->mData;
+ }
+}
+
+/**
+ * Utility class.
+ * @ingroup Database
+ */
+class MySQLField {
+ private $name, $tablename, $default, $max_length, $nullable,
+ $is_pk, $is_unique, $is_multiple, $is_key, $type;
+ function __construct ($info) {
+ $this->name = $info->name;
+ $this->tablename = $info->table;
+ $this->default = $info->def;
+ $this->max_length = $info->max_length;
+ $this->nullable = !$info->not_null;
+ $this->is_pk = $info->primary_key;
+ $this->is_unique = $info->unique_key;
+ $this->is_multiple = $info->multiple_key;
+ $this->is_key = ($this->is_pk || $this->is_unique || $this->is_multiple);
+ $this->type = $info->type;
+ }
+
+ function name() {
+ return $this->name;
+ }
+
+ function tableName() {
+ return $this->tableName;
+ }
+
+ function defaultValue() {
+ return $this->default;
+ }
+
+ function maxLength() {
+ return $this->max_length;
+ }
+
+ function nullable() {
+ return $this->nullable;
+ }
+
+ function isKey() {
+ return $this->is_key;
+ }
+
+ function isMultipleKey() {
+ return $this->is_multiple;
+ }
+
+ function type() {
+ return $this->type;
+ }
+}
+
+/******************************************************************************
+ * Error classes
+ *****************************************************************************/
+
+/**
+ * Database error base class
+ * @ingroup Database
+ */
+class DBError extends MWException {
+ public $db;
+
+ /**
+ * 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
+ */
+ function __construct( Database &$db, $error ) {
+ $this->db =& $db;
+ parent::__construct( $error );
+ }
+}
+
+/**
+ * @ingroup Database
+ */
+class DBConnectionError extends DBError {
+ public $error;
+
+ function __construct( Database &$db, $error = 'unknown error' ) {
+ $msg = 'DB connection error';
+ if ( trim( $error ) != '' ) {
+ $msg .= ": $error";
+ }
+ $this->error = $error;
+ parent::__construct( $db, $msg );
+ }
+
+ function useOutputPage() {
+ // Not likely to work
+ return false;
+ }
+
+ function useMessageCache() {
+ // Not likely to work
+ return false;
+ }
+
+ function getText() {
+ return $this->getMessage() . "\n";
+ }
+
+ function getLogMessage() {
+ # Don't send to the exception log
+ return false;
+ }
+
+ function getPageTitle() {
+ global $wgSitename;
+ return "$wgSitename has a problem";
+ }
+
+ function getHTML() {
+ global $wgTitle, $wgUseFileCache, $title, $wgInputEncoding;
+ global $wgSitename, $wgServer, $wgMessageCache;
+
+ # I give up, Brion is right. Getting the message cache to work when there is no DB is tricky.
+ # Hard coding strings instead.
+
+ $noconnect = "<p><strong>Sorry! This site is experiencing technical difficulties.</strong></p><p>Try waiting a few minutes and reloading.</p><p><small>(Can't contact the database server: $1)</small></p>";
+ $mainpage = 'Main Page';
+ $searchdisabled = <<<EOT
+<p style="margin: 1.5em 2em 1em">$wgSitename search is disabled for performance reasons. You can search via Google in the meantime.
+<span style="font-size: 89%; display: block; margin-left: .2em">Note that their indexes of $wgSitename content may be out of date.</span></p>',
+EOT;
+
+ $googlesearch = "
+<!-- SiteSearch Google -->
+<FORM method=GET action=\"http://www.google.com/search\">
+<TABLE bgcolor=\"#FFFFFF\"><tr><td>
+<A HREF=\"http://www.google.com/\">
+<IMG SRC=\"http://www.google.com/logos/Logo_40wht.gif\"
+border=\"0\" ALT=\"Google\"></A>
+</td>
+<td>
+<INPUT TYPE=text name=q size=31 maxlength=255 value=\"$1\">
+<INPUT type=submit name=btnG VALUE=\"Google Search\">
+<font size=-1>
+<input type=hidden name=domains value=\"$wgServer\"><br /><input type=radio name=sitesearch value=\"\"> WWW <input type=radio name=sitesearch value=\"$wgServer\" checked> $wgServer <br />
+<input type='hidden' name='ie' value='$2'>
+<input type='hidden' name='oe' value='$2'>
+</font>
+</td></tr></TABLE>
+</FORM>
+<!-- SiteSearch Google -->";
+ $cachederror = "The following is a cached copy of the requested page, and may not be up to date. ";
+
+ # No database access
+ if ( is_object( $wgMessageCache ) ) {
+ $wgMessageCache->disable();
+ }
+
+ if ( trim( $this->error ) == '' ) {
+ $this->error = $this->db->getProperty('mServer');
+ }
+
+ $text = str_replace( '$1', $this->error, $noconnect );
+ $text .= wfGetSiteNotice();
+
+ if($wgUseFileCache) {
+ if($wgTitle) {
+ $t =& $wgTitle;
+ } else {
+ if($title) {
+ $t = Title::newFromURL( $title );
+ } elseif (@/**/$_REQUEST['search']) {
+ $search = $_REQUEST['search'];
+ return $searchdisabled .
+ str_replace( array( '$1', '$2' ), array( htmlspecialchars( $search ),
+ $wgInputEncoding ), $googlesearch );
+ } else {
+ $t = Title::newFromText( $mainpage );
+ }
+ }
+
+ $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" .
+ $cachederror . "</b></p>\n";
+
+ $tag = '<div id="article">';
+ $text = str_replace(
+ $tag,
+ $tag . $msg,
+ $cache->fetchPageText() );
+ }
+ }
+
+ return $text;
+ }
+}
+
+/**
+ * @ingroup Database
+ */
+class DBQueryError extends DBError {
+ public $error, $errno, $sql, $fname;
+
+ function __construct( Database &$db, $error, $errno, $sql, $fname ) {
+ $message = "A database error has occurred\n" .
+ "Query: $sql\n" .
+ "Function: $fname\n" .
+ "Error: $errno $error\n";
+
+ parent::__construct( $db, $message );
+ $this->error = $error;
+ $this->errno = $errno;
+ $this->sql = $sql;
+ $this->fname = $fname;
+ }
+
+ function getText() {
+ if ( $this->useMessageCache() ) {
+ return wfMsg( 'dberrortextcl', htmlspecialchars( $this->getSQL() ),
+ htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) ) . "\n";
+ } else {
+ return $this->getMessage();
+ }
+ }
+
+ function getSQL() {
+ global $wgShowSQLErrors;
+ if( !$wgShowSQLErrors ) {
+ return $this->msg( 'sqlhidden', 'SQL hidden' );
+ } else {
+ return $this->sql;
+ }
+ }
+
+ function getLogMessage() {
+ # Don't send to the exception log
+ return false;
+ }
+
+ function getPageTitle() {
+ return $this->msg( 'databaseerror', 'Database error' );
+ }
+
+ function getHTML() {
+ if ( $this->useMessageCache() ) {
+ return wfMsgNoDB( 'dberrortext', htmlspecialchars( $this->getSQL() ),
+ htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) );
+ } else {
+ return nl2br( htmlspecialchars( $this->getMessage() ) );
+ }
+ }
+}
+
+/**
+ * @ingroup Database
+ */
+class DBUnexpectedError extends DBError {}
+
+
+/**
+ * Result wrapper for grabbing data queried by someone else
+ * @ingroup Database
+ */
+class ResultWrapper implements Iterator {
+ var $db, $result, $pos = 0, $currentRow = null;
+
+ /**
+ * Create a new result object from a result resource and a Database object
+ */
+ function ResultWrapper( $database, $result ) {
+ $this->db = $database;
+ if ( $result instanceof ResultWrapper ) {
+ $this->result = $result->result;
+ } else {
+ $this->result = $result;
+ }
+ }
+
+ /**
+ * Get the number of rows in a result object
+ */
+ function numRows() {
+ return $this->db->numRows( $this->result );
+ }
+
+ /**
+ * Fetch the next row from the given result object, in object form.
+ * Fields can be retrieved with $row->fieldname, with fields acting like
+ * member variables.
+ *
+ * @param $res SQL result object as returned from Database::query(), etc.
+ * @return MySQL row object
+ * @throws DBUnexpectedError Thrown if the database returns an error
+ */
+ function fetchObject() {
+ return $this->db->fetchObject( $this->result );
+ }
+
+ /**
+ * Fetch the next row from the given result object, in associative array
+ * form. Fields are retrieved with $row['fieldname'].
+ *
+ * @param $res SQL result object as returned from Database::query(), etc.
+ * @return MySQL row object
+ * @throws DBUnexpectedError Thrown if the database returns an error
+ */
+ function fetchRow() {
+ return $this->db->fetchRow( $this->result );
+ }
+
+ /**
+ * Free a result object
+ */
+ function free() {
+ $this->db->freeResult( $this->result );
+ unset( $this->result );
+ unset( $this->db );
+ }
+
+ /**
+ * Change the position of the cursor in a result object
+ * See mysql_data_seek()
+ */
+ function seek( $row ) {
+ $this->db->dataSeek( $this->result, $row );
+ }
+
+ /*********************
+ * Iterator functions
+ * Note that using these in combination with the non-iterator functions
+ * above may cause rows to be skipped or repeated.
+ */
+
+ function rewind() {
+ if ($this->numRows()) {
+ $this->db->dataSeek($this->result, 0);
+ }
+ $this->pos = 0;
+ $this->currentRow = null;
+ }
+
+ function current() {
+ if ( is_null( $this->currentRow ) ) {
+ $this->next();
+ }
+ return $this->currentRow;
+ }
+
+ function key() {
+ return $this->pos;
+ }
+
+ function next() {
+ $this->pos++;
+ $this->currentRow = $this->fetchObject();
+ return $this->currentRow;
+ }
+
+ function valid() {
+ return $this->current() !== false;
+ }
+}
+
+class MySQLMasterPos {
+ var $file, $pos;
+
+ function __construct( $file, $pos ) {
+ $this->file = $file;
+ $this->pos = $pos;
+ }
+
+ function __toString() {
+ return "{$this->file}/{$this->pos}";
+ }
+}
diff --git a/includes/db/DatabaseMssql.php b/includes/db/DatabaseMssql.php
new file mode 100644
index 00000000..32fe28b1
--- /dev/null
+++ b/includes/db/DatabaseMssql.php
@@ -0,0 +1,1029 @@
+<?php
+/**
+ * This script is the MSSQL Server database abstraction layer
+ *
+ * See maintenance/mssql/README for development notes and other specific information
+ * @ingroup Database
+ * @file
+ */
+
+/**
+ * @ingroup Database
+ */
+class DatabaseMssql extends Database {
+
+ var $mAffectedRows;
+ var $mLastResult;
+ var $mLastError;
+ var $mLastErrorNo;
+ var $mDatabaseFile;
+
+ /**
+ * Constructor
+ */
+ function __construct($server = false, $user = false, $password = false, $dbName = false,
+ $failFunction = false, $flags = 0, $tablePrefix = 'get from global') {
+
+ global $wgOut, $wgDBprefix, $wgCommandLineMode;
+ if (!isset($wgOut)) $wgOut = NULL; # Can't get a reference if it hasn't been set yet
+ $this->mOut =& $wgOut;
+ $this->mFailFunction = $failFunction;
+ $this->mFlags = $flags;
+
+ if ( $this->mFlags & DBO_DEFAULT ) {
+ if ( $wgCommandLineMode ) {
+ $this->mFlags &= ~DBO_TRX;
+ } else {
+ $this->mFlags |= DBO_TRX;
+ }
+ }
+
+ /** Get the default table prefix*/
+ $this->mTablePrefix = $tablePrefix == 'get from global' ? $wgDBprefix : $tablePrefix;
+
+ if ($server) $this->open($server, $user, $password, $dbName);
+
+ }
+
+ /**
+ * todo: check if these should be true like parent class
+ */
+ function implicitGroupby() { return false; }
+ function implicitOrderby() { return false; }
+
+ static function newFromParams($server, $user, $password, $dbName, $failFunction = false, $flags = 0) {
+ return new DatabaseMssql($server, $user, $password, $dbName, $failFunction, $flags);
+ }
+
+ /** Open an MSSQL database and return a resource handle to it
+ * NOTE: only $dbName is used, the other parameters are irrelevant for MSSQL databases
+ */
+ function open($server,$user,$password,$dbName) {
+ wfProfileIn(__METHOD__);
+
+ # Test for missing mysql.so
+ # First try to load it
+ if (!@extension_loaded('mssql')) {
+ @dl('mssql.so');
+ }
+
+ # Fail now
+ # Otherwise we get a suppressed fatal error, which is very hard to track down
+ if (!function_exists( 'mssql_connect')) {
+ throw new DBConnectionError( $this, "MSSQL functions missing, have you compiled PHP with the --with-mssql option?\n" );
+ }
+
+ $this->close();
+ $this->mServer = $server;
+ $this->mUser = $user;
+ $this->mPassword = $password;
+ $this->mDBname = $dbName;
+
+ 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.
+ $this->mConn = false;
+ $max = 3;
+ for ( $i = 0; $i < $max && !$this->mConn; $i++ ) {
+ if ( $i > 1 ) {
+ usleep( 1000 );
+ }
+ if ($this->mFlags & DBO_PERSISTENT) {
+ @/**/$this->mConn = mssql_pconnect($server, $user, $password);
+ } else {
+ # Create a new connection...
+ @/**/$this->mConn = mssql_connect($server, $user, $password, true);
+ }
+ }
+
+ wfProfileOut("dbconnect-$server");
+
+ if ($dbName != '') {
+ if ($this->mConn !== false) {
+ $success = @/**/mssql_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)."...\n");
+ $success = false;
+ }
+ } else {
+ # Delay USE query
+ $success = (bool)$this->mConn;
+ }
+
+ if (!$success) $this->reportConnectionError();
+ $this->mOpened = $success;
+ wfProfileOut(__METHOD__);
+ return $success;
+ }
+
+ /**
+ * Close an MSSQL database
+ */
+ function close() {
+ $this->mOpened = false;
+ if ($this->mConn) {
+ if ($this->trxLevel()) $this->immediateCommit();
+ return mssql_close($this->mConn);
+ } else return true;
+ }
+
+ /**
+ * - MSSQL doesn't seem to do buffered results
+ * - the trasnaction syntax is modified here to avoid having to replicate
+ * Database::query which uses BEGIN, COMMIT, ROLLBACK
+ */
+ function doQuery($sql) {
+ if ($sql == 'BEGIN' || $sql == 'COMMIT' || $sql == 'ROLLBACK') return true; # $sql .= ' TRANSACTION';
+ $sql = preg_replace('|[^\x07-\x7e]|','?',$sql); # TODO: need to fix unicode - just removing it here while testing
+ $ret = mssql_query($sql, $this->mConn);
+ if ($ret === false) {
+ $err = mssql_get_last_message();
+ if ($err) $this->mlastError = $err;
+ $row = mssql_fetch_row(mssql_query('select @@ERROR'));
+ if ($row[0]) $this->mlastErrorNo = $row[0];
+ } else $this->mlastErrorNo = false;
+ return $ret;
+ }
+
+ /**#@+
+ * @param mixed $res A SQL result
+ */
+ /**
+ * Free a result object
+ */
+ function freeResult( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ if ( !@/**/mssql_free_result( $res ) ) {
+ throw new DBUnexpectedError( $this, "Unable to free MSSQL result" );
+ }
+ }
+
+ /**
+ * Fetch the next row from the given result object, in object form.
+ * Fields can be retrieved with $row->fieldname, with fields acting like
+ * member variables.
+ *
+ * @param $res SQL result object as returned from Database::query(), etc.
+ * @return MySQL row object
+ * @throws DBUnexpectedError Thrown if the database returns an error
+ */
+ function fetchObject( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ @/**/$row = mssql_fetch_object( $res );
+ if ( $this->lastErrno() ) {
+ throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) );
+ }
+ return $row;
+ }
+
+ /**
+ * Fetch the next row from the given result object, in associative array
+ * form. Fields are retrieved with $row['fieldname'].
+ *
+ * @param $res SQL result object as returned from Database::query(), etc.
+ * @return MySQL row object
+ * @throws DBUnexpectedError Thrown if the database returns an error
+ */
+ function fetchRow( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ @/**/$row = mssql_fetch_array( $res );
+ if ( $this->lastErrno() ) {
+ throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) );
+ }
+ return $row;
+ }
+
+ /**
+ * Get the number of rows in a result object
+ */
+ function numRows( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ @/**/$n = mssql_num_rows( $res );
+ if ( $this->lastErrno() ) {
+ throw new DBUnexpectedError( $this, 'Error in numRows(): ' . htmlspecialchars( $this->lastError() ) );
+ }
+ return $n;
+ }
+
+ /**
+ * Get the number of fields in a result object
+ * See documentation for mysql_num_fields()
+ */
+ function numFields( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return mssql_num_fields( $res );
+ }
+
+ /**
+ * Get a field name in a result object
+ * See documentation for mysql_field_name():
+ * http://www.php.net/mysql_field_name
+ */
+ function fieldName( $res, $n ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return mssql_field_name( $res, $n );
+ }
+
+ /**
+ * Get the inserted value of an auto-increment row
+ *
+ * The value inserted should be fetched from nextSequenceValue()
+ *
+ * Example:
+ * $id = $dbw->nextSequenceValue('page_page_id_seq');
+ * $dbw->insert('page',array('page_id' => $id));
+ * $id = $dbw->insertId();
+ */
+ function insertId() {
+ $row = mssql_fetch_row(mssql_query('select @@IDENTITY'));
+ return $row[0];
+ }
+
+ /**
+ * Change the position of the cursor in a result object
+ * See mysql_data_seek()
+ */
+ function dataSeek( $res, $row ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return mssql_data_seek( $res, $row );
+ }
+
+ /**
+ * Get the last error number
+ */
+ function lastErrno() {
+ return $this->mlastErrorNo;
+ }
+
+ /**
+ * Get a description of the last error
+ */
+ function lastError() {
+ return $this->mlastError;
+ }
+
+ /**
+ * Get the number of rows affected by the last write query
+ */
+ function affectedRows() {
+ return mssql_rows_affected( $this->mConn );
+ }
+
+ /**
+ * Simple UPDATE wrapper
+ * Usually aborts on failure
+ * If errors are explicitly ignored, returns success
+ *
+ * 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' )
+ {
+ if ($value == "NULL") $value = "''"; # see comments in makeListWithoutNulls()
+ $table = $this->tableName( $table );
+ $sql = "UPDATE $table SET $var = '" .
+ $this->strencode( $value ) . "' WHERE ($cond)";
+ return (bool)$this->query( $sql, $fname );
+ }
+
+ /**
+ * Simple SELECT wrapper, returns a single field, input must be encoded
+ * Usually aborts on failure
+ * If errors are explicitly ignored, returns FALSE on failure
+ */
+ function selectField( $table, $var, $cond='', $fname = 'Database::selectField', $options = array() ) {
+ if ( !is_array( $options ) ) {
+ $options = array( $options );
+ }
+ $options['LIMIT'] = 1;
+
+ $res = $this->select( $table, $var, $cond, $fname, $options );
+ if ( $res === false || !$this->numRows( $res ) ) {
+ return false;
+ }
+ $row = $this->fetchRow( $res );
+ if ( $row !== false ) {
+ $this->freeResult( $res );
+ return $row[0];
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Returns an optional USE INDEX clause to go after the table, and a
+ * string to go at the end of the query
+ *
+ * @private
+ *
+ * @param array $options an associative array of options to be turned into
+ * an SQL query, valid keys are listed in the function.
+ * @return array
+ */
+ function makeSelectOptions( $options ) {
+ $preLimitTail = $postLimitTail = '';
+ $startOpts = '';
+
+ $noKeyOptions = array();
+ foreach ( $options as $key => $option ) {
+ if ( is_numeric( $key ) ) {
+ $noKeyOptions[$option] = true;
+ }
+ }
+
+ if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY {$options['GROUP BY']}";
+ if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}";
+ if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY {$options['ORDER BY']}";
+
+ //if (isset($options['LIMIT'])) {
+ // $tailOpts .= $this->limitResult('', $options['LIMIT'],
+ // isset($options['OFFSET']) ? $options['OFFSET']
+ // : false);
+ //}
+
+ if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $postLimitTail .= ' FOR UPDATE';
+ if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $postLimitTail .= ' LOCK IN SHARE MODE';
+ if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
+
+ # Various MySQL extensions
+ if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) $startOpts .= ' /*! STRAIGHT_JOIN */';
+ if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) $startOpts .= ' HIGH_PRIORITY';
+ if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) $startOpts .= ' SQL_BIG_RESULT';
+ if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) $startOpts .= ' SQL_BUFFER_RESULT';
+ if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) $startOpts .= ' SQL_SMALL_RESULT';
+ if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) $startOpts .= ' SQL_CALC_FOUND_ROWS';
+ if ( isset( $noKeyOptions['SQL_CACHE'] ) ) $startOpts .= ' SQL_CACHE';
+ if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) $startOpts .= ' SQL_NO_CACHE';
+
+ if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) {
+ $useIndex = $this->useIndexClause( $options['USE INDEX'] );
+ } else {
+ $useIndex = '';
+ }
+
+ return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
+ }
+
+ /**
+ * 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
+ * @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() )
+ {
+ if( is_array( $vars ) ) {
+ $vars = implode( ',', $vars );
+ }
+ if( !is_array( $options ) ) {
+ $options = array( $options );
+ }
+ if( is_array( $table ) ) {
+ if ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
+ $from = ' FROM ' . $this->tableNamesWithUseIndex( $table, $options['USE INDEX'] );
+ else
+ $from = ' FROM ' . implode( ',', array_map( array( &$this, 'tableName' ), $table ) );
+ } elseif ($table!='') {
+ if ($table{0}==' ') {
+ $from = ' FROM ' . $table;
+ } else {
+ $from = ' FROM ' . $this->tableName( $table );
+ }
+ } else {
+ $from = '';
+ }
+
+ list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) = $this->makeSelectOptions( $options );
+
+ if( !empty( $conds ) ) {
+ if ( is_array( $conds ) ) {
+ $conds = $this->makeList( $conds, LIST_AND );
+ }
+ $sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $preLimitTail";
+ } else {
+ $sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail";
+ }
+
+ if (isset($options['LIMIT']))
+ $sql = $this->limitResult($sql, $options['LIMIT'],
+ isset($options['OFFSET']) ? $options['OFFSET'] : false);
+ $sql = "$sql $postLimitTail";
+
+ if (isset($options['EXPLAIN'])) {
+ $sql = 'EXPLAIN ' . $sql;
+ }
+ return $this->query( $sql, $fname );
+ }
+
+ /**
+ * Estimate rows in dataset
+ * Returns estimated count, based on EXPLAIN output
+ * Takes same arguments as Database::select()
+ */
+ function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) {
+ $rows = 0;
+ $res = $this->select ($table, 'COUNT(*)', $conds, $fname, $options );
+ if ($res) {
+ $row = $this->fetchObject($res);
+ $rows = $row[0];
+ }
+ $this->freeResult($res);
+ return $rows;
+ }
+
+ /**
+ * Determines whether a field exists in a table
+ * Usually aborts on failure
+ * If errors are explicitly ignored, returns NULL on failure
+ */
+ function fieldExists( $table, $field, $fname = 'Database::fieldExists' ) {
+ $table = $this->tableName( $table );
+ $sql = "SELECT TOP 1 * FROM $table";
+ $res = $this->query( $sql, 'Database::fieldExists' );
+
+ $found = false;
+ while ( $row = $this->fetchArray( $res ) ) {
+ if ( isset($row[$field]) ) {
+ $found = true;
+ break;
+ }
+ }
+
+ $this->freeResult( $res );
+ return $found;
+ }
+
+ /**
+ * Get information about an index into an object
+ * Returns false if the index does not exist
+ */
+ function indexInfo( $table, $index, $fname = 'Database::indexInfo' ) {
+
+ throw new DBUnexpectedError( $this, 'Database::indexInfo called which is not supported yet' );
+ return NULL;
+
+ $table = $this->tableName( $table );
+ $sql = 'SHOW INDEX FROM '.$table;
+ $res = $this->query( $sql, $fname );
+ if ( !$res ) {
+ return NULL;
+ }
+
+ $result = array();
+ while ( $row = $this->fetchObject( $res ) ) {
+ if ( $row->Key_name == $index ) {
+ $result[] = $row;
+ }
+ }
+ $this->freeResult($res);
+
+ return empty($result) ? false : $result;
+ }
+
+ /**
+ * Query whether a given table exists
+ */
+ function tableExists( $table ) {
+ $table = $this->tableName( $table );
+ $res = $this->query( "SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '$table'" );
+ $exist = ($res->numRows() > 0);
+ $this->freeResult($res);
+ return $exist;
+ }
+
+ /**
+ * mysql_fetch_field() wrapper
+ * Returns false if the field doesn't exist
+ *
+ * @param $table
+ * @param $field
+ */
+ function fieldInfo( $table, $field ) {
+ $table = $this->tableName( $table );
+ $res = $this->query( "SELECT TOP 1 * FROM $table" );
+ $n = mssql_num_fields( $res->result );
+ for( $i = 0; $i < $n; $i++ ) {
+ $meta = mssql_fetch_field( $res->result, $i );
+ if( $field == $meta->name ) {
+ return new MSSQLField($meta);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * mysql_field_type() wrapper
+ */
+ function fieldType( $res, $index ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return mssql_field_type( $res, $index );
+ }
+
+ /**
+ * INSERT wrapper, inserts an array into a table
+ *
+ * $a may be a single associative array, or an array of these with numeric keys, for
+ * multi-row insert.
+ *
+ * Usually aborts on failure
+ * If errors are explicitly ignored, returns success
+ *
+ * Same as parent class implementation except that it removes primary key from column lists
+ * because MSSQL doesn't support writing nulls to IDENTITY (AUTO_INCREMENT) columns
+ */
+ function insert( $table, $a, $fname = 'Database::insert', $options = array() ) {
+ # No rows to insert, easy just return now
+ if ( !count( $a ) ) {
+ return true;
+ }
+ $table = $this->tableName( $table );
+ if ( !is_array( $options ) ) {
+ $options = array( $options );
+ }
+
+ # todo: need to record primary keys at table create time, and remove NULL assignments to them
+ if ( isset( $a[0] ) && is_array( $a[0] ) ) {
+ $multi = true;
+ $keys = array_keys( $a[0] );
+# if (ereg('_id$',$keys[0])) {
+ foreach ($a as $i) {
+ if (is_null($i[$keys[0]])) unset($i[$keys[0]]); # remove primary-key column from multiple insert lists if empty value
+ }
+# }
+ $keys = array_keys( $a[0] );
+ } else {
+ $multi = false;
+ $keys = array_keys( $a );
+# if (ereg('_id$',$keys[0]) && empty($a[$keys[0]])) unset($a[$keys[0]]); # remove primary-key column from insert list if empty value
+ if (is_null($a[$keys[0]])) unset($a[$keys[0]]); # remove primary-key column from insert list if empty value
+ $keys = array_keys( $a );
+ }
+
+ # handle IGNORE option
+ # example:
+ # MySQL: INSERT IGNORE INTO user_groups (ug_user,ug_group) VALUES ('1','sysop')
+ # MSSQL: IF NOT EXISTS (SELECT * FROM user_groups WHERE ug_user = '1') INSERT INTO user_groups (ug_user,ug_group) VALUES ('1','sysop')
+ $ignore = in_array('IGNORE',$options);
+
+ # remove IGNORE from options list
+ if ($ignore) {
+ $oldoptions = $options;
+ $options = array();
+ foreach ($oldoptions as $o) if ($o != 'IGNORE') $options[] = $o;
+ }
+
+ $keylist = implode(',', $keys);
+ $sql = 'INSERT '.implode(' ', $options)." INTO $table (".implode(',', $keys).') VALUES ';
+ if ($multi) {
+ if ($ignore) {
+ # If multiple and ignore, then do each row as a separate conditional insert
+ foreach ($a as $row) {
+ $prival = $row[$keys[0]];
+ $sql = "IF NOT EXISTS (SELECT * FROM $table WHERE $keys[0] = '$prival') $sql";
+ if (!$this->query("$sql (".$this->makeListWithoutNulls($row).')', $fname)) return false;
+ }
+ return true;
+ } else {
+ $first = true;
+ foreach ($a as $row) {
+ if ($first) $first = false; else $sql .= ',';
+ $sql .= '('.$this->makeListWithoutNulls($row).')';
+ }
+ }
+ } else {
+ if ($ignore) {
+ $prival = $a[$keys[0]];
+ $sql = "IF NOT EXISTS (SELECT * FROM $table WHERE $keys[0] = '$prival') $sql";
+ }
+ $sql .= '('.$this->makeListWithoutNulls($a).')';
+ }
+ return (bool)$this->query( $sql, $fname );
+ }
+
+ /**
+ * MSSQL doesn't allow implicit casting of NULL's into non-null values for NOT NULL columns
+ * for now I've just converted the NULL's in the lists for updates and inserts into empty strings
+ * which get implicitly casted to 0 for numeric columns
+ * NOTE: the set() method above converts NULL to empty string as well but not via this method
+ */
+ function makeListWithoutNulls($a, $mode = LIST_COMMA) {
+ return str_replace("NULL","''",$this->makeList($a,$mode));
+ }
+
+ /**
+ * 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
+ * more of IGNORE, LOW_PRIORITY
+ * @return bool
+ */
+ function update( $table, $values, $conds, $fname = 'Database::update', $options = array() ) {
+ $table = $this->tableName( $table );
+ $opts = $this->makeUpdateOptions( $options );
+ $sql = "UPDATE $opts $table SET " . $this->makeListWithoutNulls( $values, LIST_SET );
+ if ( $conds != '*' ) {
+ $sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
+ }
+ return $this->query( $sql, $fname );
+ }
+
+ /**
+ * Make UPDATE options for the Database::update function
+ *
+ * @private
+ * @param array $options The options passed to Database::update
+ * @return string
+ */
+ function makeUpdateOptions( $options ) {
+ if( !is_array( $options ) ) {
+ $options = array( $options );
+ }
+ $opts = array();
+ if ( in_array( 'LOW_PRIORITY', $options ) )
+ $opts[] = $this->lowPriorityOption();
+ if ( in_array( 'IGNORE', $options ) )
+ $opts[] = 'IGNORE';
+ return implode(' ', $opts);
+ }
+
+ /**
+ * Change the current database
+ */
+ function selectDB( $db ) {
+ $this->mDBname = $db;
+ return mssql_select_db( $db, $this->mConn );
+ }
+
+ /**
+ * MSSQL has a problem with the backtick quoting, so all this does is ensure the prefix is added exactly once
+ */
+ function tableName($name) {
+ return strpos($name, $this->mTablePrefix) === 0 ? $name : "{$this->mTablePrefix}$name";
+ }
+
+ /**
+ * MSSQL doubles quotes instead of escaping them
+ * @param string $s String to be slashed.
+ * @return string slashed string.
+ */
+ function strencode($s) {
+ return str_replace("'","''",$s);
+ }
+
+ /**
+ * USE INDEX clause
+ */
+ function useIndexClause( $index ) {
+ return "";
+ }
+
+ /**
+ * REPLACE query wrapper
+ * PostgreSQL simulates this with a DELETE followed by INSERT
+ * $row is the row to insert, an associative array
+ * $uniqueIndexes is an array of indexes. Each element may be either a
+ * field name or an array of field names
+ *
+ * It may be more efficient to leave off unique indexes which are unlikely to collide.
+ * However if you do this, you run the risk of encountering errors which wouldn't have
+ * occurred in MySQL
+ *
+ * @todo migrate comment to phodocumentor format
+ */
+ function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) {
+ $table = $this->tableName( $table );
+
+ # Single row case
+ if ( !is_array( reset( $rows ) ) ) {
+ $rows = array( $rows );
+ }
+
+ $sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) .') VALUES ';
+ $first = true;
+ foreach ( $rows as $row ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $sql .= ',';
+ }
+ $sql .= '(' . $this->makeList( $row ) . ')';
+ }
+ return $this->query( $sql, $fname );
+ }
+
+ /**
+ * DELETE where the condition is a join
+ * MySQL does this with a multi-table DELETE syntax, PostgreSQL does it with sub-selects
+ *
+ * For safety, an empty $conds will not delete everything. If you want to delete all rows where the
+ * join condition matches, set $conds='*'
+ *
+ * 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
+ */
+ function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'Database::deleteJoin' ) {
+ if ( !$conds ) {
+ throw new DBUnexpectedError( $this, 'Database::deleteJoin() called with empty $conds' );
+ }
+
+ $delTable = $this->tableName( $delTable );
+ $joinTable = $this->tableName( $joinTable );
+ $sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar ";
+ if ( $conds != '*' ) {
+ $sql .= ' AND ' . $this->makeList( $conds, LIST_AND );
+ }
+
+ return $this->query( $sql, $fname );
+ }
+
+ /**
+ * Returns the size of a text field, or -1 for "unlimited"
+ */
+ function textFieldSize( $table, $field ) {
+ $table = $this->tableName( $table );
+ $sql = "SELECT TOP 1 * FROM $table;";
+ $res = $this->query( $sql, 'Database::textFieldSize' );
+ $row = $this->fetchObject( $res );
+ $this->freeResult( $res );
+
+ $m = array();
+ if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
+ $size = $m[1];
+ } else {
+ $size = -1;
+ }
+ return $size;
+ }
+
+ /**
+ * @return string Returns the text of the low priority option if it is supported, or a blank string otherwise
+ */
+ function lowPriorityOption() {
+ return 'LOW_PRIORITY';
+ }
+
+ /**
+ * INSERT SELECT wrapper
+ * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...)
+ * Source items may be literals rather than field names, but strings should be quoted with Database::addQuotes()
+ * $conds may be "*" to copy the whole table
+ * srcTable may be an array of tables.
+ */
+ function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'Database::insertSelect',
+ $insertOptions = array(), $selectOptions = array() )
+ {
+ $destTable = $this->tableName( $destTable );
+ if ( is_array( $insertOptions ) ) {
+ $insertOptions = implode( ' ', $insertOptions );
+ }
+ if( !is_array( $selectOptions ) ) {
+ $selectOptions = array( $selectOptions );
+ }
+ list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
+ if( is_array( $srcTable ) ) {
+ $srcTable = implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) );
+ } else {
+ $srcTable = $this->tableName( $srcTable );
+ }
+ $sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
+ " SELECT $startOpts " . implode( ',', $varMap ) .
+ " FROM $srcTable $useIndex ";
+ if ( $conds != '*' ) {
+ $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
+ }
+ $sql .= " $tailOpts";
+ return $this->query( $sql, $fname );
+ }
+
+ /**
+ * Construct a LIMIT query with optional offset
+ * This is used for query pages
+ * $sql string SQL query we will append the limit to
+ * $limit integer the SQL limit
+ * $offset integer the SQL offset (default false)
+ */
+ function limitResult($sql, $limit, $offset=false) {
+ if( !is_numeric($limit) ) {
+ throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
+ }
+ if ($offset) {
+ throw new DBUnexpectedError( $this, 'Database::limitResult called with non-zero offset which is not supported yet' );
+ } else {
+ $sql = ereg_replace("^SELECT", "SELECT TOP $limit", $sql);
+ }
+ return $sql;
+ }
+
+ /**
+ * 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
+ * @return string SQL fragment
+ */
+ function conditional( $cond, $trueVal, $falseVal ) {
+ return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
+ }
+
+ /**
+ * Should determine if the last failure was due to a deadlock
+ * @return bool
+ */
+ function wasDeadlock() {
+ return $this->lastErrno() == 1205;
+ }
+
+ /**
+ * Begin a transaction, committing any previously open transaction
+ * @deprecated use begin()
+ */
+ function immediateBegin( $fname = 'Database::immediateBegin' ) {
+ $this->begin();
+ }
+
+ /**
+ * Commit transaction, if one is open
+ * @deprecated use commit()
+ */
+ function immediateCommit( $fname = 'Database::immediateCommit' ) {
+ $this->commit();
+ }
+
+ /**
+ * Return MW-style timestamp used for MySQL schema
+ */
+ function timestamp( $ts=0 ) {
+ return wfTimestamp(TS_MW,$ts);
+ }
+
+ /**
+ * Local database timestamp format or null
+ */
+ function timestampOrNull( $ts = null ) {
+ if( is_null( $ts ) ) {
+ return null;
+ } else {
+ return $this->timestamp( $ts );
+ }
+ }
+
+ /**
+ * @return string wikitext of a link to the server software's web site
+ */
+ function getSoftwareLink() {
+ return "[http://www.microsoft.com/sql/default.mspx Microsoft SQL Server 2005 Home]";
+ }
+
+ /**
+ * @return string Version information from the database
+ */
+ function getServerVersion() {
+ $row = mssql_fetch_row(mssql_query('select @@VERSION'));
+ return ereg("^(.+[0-9]+\\.[0-9]+\\.[0-9]+) ",$row[0],$m) ? $m[1] : $row[0];
+ }
+
+ function limitResultForUpdate($sql, $num) {
+ return $sql;
+ }
+
+ /**
+ * not done
+ */
+ public function setTimeout($timeout) { return; }
+
+ function ping() {
+ wfDebug("Function ping() not written for MSSQL yet");
+ return true;
+ }
+
+ /**
+ * How lagged is this slave?
+ */
+ public function getLag() {
+ return 0;
+ }
+
+ /**
+ * Called by the installer script
+ * - this is the same way as DatabasePostgresql.php, MySQL reads in tables.sql and interwiki.sql using dbsource (which calls db->sourceFile)
+ */
+ public function setup_database() {
+ global $IP,$wgDBTableOptions;
+ $wgDBTableOptions = '';
+ $mysql_tmpl = "$IP/maintenance/tables.sql";
+ $mysql_iw = "$IP/maintenance/interwiki.sql";
+ $mssql_tmpl = "$IP/maintenance/mssql/tables.sql";
+
+ # Make an MSSQL template file if it doesn't exist (based on the same one MySQL uses to create a new wiki db)
+ if (!file_exists($mssql_tmpl)) { # todo: make this conditional again
+ $sql = file_get_contents($mysql_tmpl);
+ $sql = preg_replace('/^\s*--.*?$/m','',$sql); # strip comments
+ $sql = preg_replace('/^\s*(UNIQUE )?(INDEX|KEY|FULLTEXT).+?$/m', '', $sql); # These indexes should be created with a CREATE INDEX query
+ $sql = preg_replace('/(\sKEY) [^\(]+\(/is', '$1 (', $sql); # "KEY foo (foo)" should just be "KEY (foo)"
+ $sql = preg_replace('/(varchar\([0-9]+\))\s+binary/i', '$1', $sql); # "varchar(n) binary" cannot be followed by "binary"
+ $sql = preg_replace('/(var)?binary\(([0-9]+)\)/ie', '"varchar(".strlen(pow(2,$2)).")"', $sql); # use varchar(chars) not binary(bits)
+ $sql = preg_replace('/ (var)?binary/i', ' varchar', $sql); # use varchar not binary
+ $sql = preg_replace('/(varchar\([0-9]+\)(?! N))/', '$1 NULL', $sql); # MSSQL complains if NULL is put into a varchar
+ #$sql = preg_replace('/ binary/i',' varchar',$sql); # MSSQL binary's can't be assigned with strings, so use varchar's instead
+ #$sql = preg_replace('/(binary\([0-9]+\) (NOT NULL )?default) [\'"].*?[\'"]/i','$1 0',$sql); # binary default cannot be string
+ $sql = preg_replace('/[a-z]*(blob|text)([ ,])/i', 'text$2', $sql); # no BLOB types in MSSQL
+ $sql = preg_replace('/\).+?;/',');', $sql); # remove all table options
+ $sql = preg_replace('/ (un)?signed/i', '', $sql);
+ $sql = preg_replace('/ENUM\(.+?\)/','TEXT',$sql); # Make ENUM's into TEXT's
+ $sql = str_replace(' bool ', ' bit ', $sql);
+ $sql = str_replace('auto_increment', 'IDENTITY(1,1)', $sql);
+ #$sql = preg_replace('/NOT NULL(?! IDENTITY)/', 'NULL', $sql); # Allow NULL's for non IDENTITY columns
+
+ # Tidy up and write file
+ $sql = preg_replace('/,\s*\)/s', "\n)", $sql); # Remove spurious commas left after INDEX removals
+ $sql = preg_replace('/^\s*^/m', '', $sql); # Remove empty lines
+ $sql = preg_replace('/;$/m', ";\n", $sql); # Separate each statement with an empty line
+ file_put_contents($mssql_tmpl, $sql);
+ }
+
+ # Parse the MSSQL template replacing inline variables such as /*$wgDBprefix*/
+ $err = $this->sourceFile($mssql_tmpl);
+ if ($err !== true) $this->reportQueryError($err,0,$sql,__FUNCTION__);
+
+ # Use DatabasePostgres's code to populate interwiki from MySQL template
+ $f = fopen($mysql_iw,'r');
+ if ($f == false) dieout("<li>Could not find the interwiki.sql file");
+ $sql = "INSERT INTO {$this->mTablePrefix}interwiki(iw_prefix,iw_url,iw_local) VALUES ";
+ while (!feof($f)) {
+ $line = fgets($f,1024);
+ $matches = array();
+ if (!preg_match('/^\s*(\(.+?),(\d)\)/', $line, $matches)) continue;
+ $this->query("$sql $matches[1],$matches[2])");
+ }
+ }
+
+ /**
+ * No-op lock functions
+ */
+ public function lock( $lockName, $method ) {
+ return true;
+ }
+ public function unlock( $lockName, $method ) {
+ return true;
+ }
+
+}
+
+/**
+ * @ingroup Database
+ */
+class MSSQLField extends MySQLField {
+
+ function __construct() {
+ }
+
+ static function fromText($db, $table, $field) {
+ $n = new MSSQLField;
+ $n->name = $field;
+ $n->tablename = $table;
+ return $n;
+ }
+
+} // end DatabaseMssql class
+
diff --git a/includes/db/DatabaseOracle.php b/includes/db/DatabaseOracle.php
new file mode 100644
index 00000000..f4dbac71
--- /dev/null
+++ b/includes/db/DatabaseOracle.php
@@ -0,0 +1,720 @@
+<?php
+/**
+ * @ingroup Database
+ * @file
+ */
+
+/**
+ * This is the Oracle database abstraction layer.
+ * @ingroup Database
+ */
+class ORABlob {
+ var $mData;
+
+ function __construct($data) {
+ $this->mData = $data;
+ }
+
+ function getData() {
+ return $this->mData;
+ }
+}
+
+/**
+ * The oci8 extension is fairly weak and doesn't support oci_num_rows, among
+ * other things. We use a wrapper class to handle that and other
+ * Oracle-specific bits, like converting column names back to lowercase.
+ * @ingroup Database
+ */
+class ORAResult {
+ private $rows;
+ private $cursor;
+ private $stmt;
+ private $nrows;
+ private $db;
+
+ function __construct(&$db, $stmt) {
+ $this->db =& $db;
+ if (($this->nrows = oci_fetch_all($stmt, $this->rows, 0, -1, OCI_FETCHSTATEMENT_BY_ROW | OCI_NUM)) === false) {
+ $e = oci_error($stmt);
+ $db->reportQueryError($e['message'], $e['code'], '', __FUNCTION__);
+ return;
+ }
+
+ $this->cursor = 0;
+ $this->stmt = $stmt;
+ }
+
+ function free() {
+ oci_free_statement($this->stmt);
+ }
+
+ function seek($row) {
+ $this->cursor = min($row, $this->nrows);
+ }
+
+ function numRows() {
+ return $this->nrows;
+ }
+
+ function numFields() {
+ return oci_num_fields($this->stmt);
+ }
+
+ function fetchObject() {
+ if ($this->cursor >= $this->nrows)
+ return false;
+
+ $row = $this->rows[$this->cursor++];
+ $ret = new stdClass();
+ foreach ($row as $k => $v) {
+ $lc = strtolower(oci_field_name($this->stmt, $k + 1));
+ $ret->$lc = $v;
+ }
+
+ return $ret;
+ }
+
+ function fetchAssoc() {
+ if ($this->cursor >= $this->nrows)
+ return false;
+
+ $row = $this->rows[$this->cursor++];
+ $ret = array();
+ foreach ($row as $k => $v) {
+ $lc = strtolower(oci_field_name($this->stmt, $k + 1));
+ $ret[$lc] = $v;
+ $ret[$k] = $v;
+ }
+ return $ret;
+ }
+}
+
+/**
+ * @ingroup Database
+ */
+class DatabaseOracle extends Database {
+ var $mInsertId = NULL;
+ var $mLastResult = NULL;
+ var $numeric_version = NULL;
+ var $lastResult = null;
+ var $cursor = 0;
+ var $mAffectedRows;
+
+ function DatabaseOracle($server = false, $user = false, $password = false, $dbName = false,
+ $failFunction = false, $flags = 0 )
+ {
+
+ global $wgOut;
+ # Can't get a reference if it hasn't been set yet
+ if ( !isset( $wgOut ) ) {
+ $wgOut = NULL;
+ }
+ $this->mOut =& $wgOut;
+ $this->mFailFunction = $failFunction;
+ $this->mFlags = $flags;
+ $this->open( $server, $user, $password, $dbName);
+
+ }
+
+ function cascadingDeletes() {
+ return true;
+ }
+ function cleanupTriggers() {
+ return true;
+ }
+ function strictIPs() {
+ return true;
+ }
+ function realTimestamps() {
+ return true;
+ }
+ function implicitGroupby() {
+ return false;
+ }
+ function implicitOrderby() {
+ return false;
+ }
+ function searchableIPs() {
+ return true;
+ }
+
+ static function newFromParams( $server = false, $user = false, $password = false, $dbName = false,
+ $failFunction = false, $flags = 0)
+ {
+ return new DatabaseOracle( $server, $user, $password, $dbName, $failFunction, $flags );
+ }
+
+ /**
+ * Usually aborts on failure
+ * If the failFunction is set to a non-zero integer, returns success
+ */
+ function open( $server, $user, $password, $dbName ) {
+ if ( !function_exists( 'oci_connect' ) ) {
+ throw new DBConnectionError( $this, "Oracle functions missing, have you compiled PHP with the --with-oci8 option?\n (Note: if you recently installed PHP, you may need to restart your webserver and database)\n" );
+ }
+
+ # Needed for proper UTF-8 functionality
+ putenv("NLS_LANG=AMERICAN_AMERICA.AL32UTF8");
+
+ $this->close();
+ $this->mServer = $server;
+ $this->mUser = $user;
+ $this->mPassword = $password;
+ $this->mDBname = $dbName;
+
+ if (!strlen($user)) { ## e.g. the class is being loaded
+ return;
+ }
+
+ error_reporting( E_ALL );
+ $this->mConn = oci_connect($user, $password, $dbName);
+
+ if ($this->mConn == false) {
+ wfDebug("DB connection error\n");
+ wfDebug("Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n");
+ wfDebug($this->lastError()."\n");
+ return false;
+ }
+
+ $this->mOpened = true;
+ return $this->mConn;
+ }
+
+ /**
+ * Closes a database connection, if it is open
+ * Returns success, true if already closed
+ */
+ function close() {
+ $this->mOpened = false;
+ if ( $this->mConn ) {
+ return oci_close( $this->mConn );
+ } else {
+ return true;
+ }
+ }
+
+ function execFlags() {
+ return $this->mTrxLevel ? OCI_DEFAULT : OCI_COMMIT_ON_SUCCESS;
+ }
+
+ function doQuery($sql) {
+ wfDebug("SQL: [$sql]\n");
+ if (!mb_check_encoding($sql)) {
+ throw new MWException("SQL encoding is invalid");
+ }
+
+ if (($this->mLastResult = $stmt = oci_parse($this->mConn, $sql)) === false) {
+ $e = oci_error($this->mConn);
+ $this->reportQueryError($e['message'], $e['code'], $sql, __FUNCTION__);
+ }
+
+ if (oci_execute($stmt, $this->execFlags()) == false) {
+ $e = oci_error($stmt);
+ $this->reportQueryError($e['message'], $e['code'], $sql, __FUNCTION__);
+ }
+ if (oci_statement_type($stmt) == "SELECT")
+ return new ORAResult($this, $stmt);
+ else {
+ $this->mAffectedRows = oci_num_rows($stmt);
+ return true;
+ }
+ }
+
+ function queryIgnore($sql, $fname = '') {
+ return $this->query($sql, $fname, true);
+ }
+
+ function freeResult($res) {
+ $res->free();
+ }
+
+ function fetchObject($res) {
+ return $res->fetchObject();
+ }
+
+ function fetchRow($res) {
+ return $res->fetchAssoc();
+ }
+
+ function numRows($res) {
+ return $res->numRows();
+ }
+
+ function numFields($res) {
+ return $res->numFields();
+ }
+
+ function fieldName($stmt, $n) {
+ return pg_field_name($stmt, $n);
+ }
+
+ /**
+ * This must be called after nextSequenceVal
+ */
+ function insertId() {
+ return $this->mInsertId;
+ }
+
+ function dataSeek($res, $row) {
+ $res->seek($row);
+ }
+
+ function lastError() {
+ if ($this->mConn === false)
+ $e = oci_error();
+ else
+ $e = oci_error($this->mConn);
+ return $e['message'];
+ }
+
+ function lastErrno() {
+ if ($this->mConn === false)
+ $e = oci_error();
+ else
+ $e = oci_error($this->mConn);
+ return $e['code'];
+ }
+
+ function affectedRows() {
+ return $this->mAffectedRows;
+ }
+
+ /**
+ * Returns information about an index
+ * If errors are explicitly ignored, returns NULL on failure
+ */
+ function indexInfo( $table, $index, $fname = 'Database::indexExists' ) {
+ return false;
+ }
+
+ function indexUnique ($table, $index, $fname = 'Database::indexUnique' ) {
+ return false;
+ }
+
+ function insert( $table, $a, $fname = 'Database::insert', $options = array() ) {
+ if (!is_array($options))
+ $options = array($options);
+
+ #if (in_array('IGNORE', $options))
+ # $oldIgnore = $this->ignoreErrors(true);
+
+ # IGNORE is performed using single-row inserts, ignoring errors in each
+ # FIXME: need some way to distiguish between key collision and other types of error
+ //$oldIgnore = $this->ignoreErrors(true);
+ if (!is_array(reset($a))) {
+ $a = array($a);
+ }
+ foreach ($a as $row) {
+ $this->insertOneRow($table, $row, $fname);
+ }
+ //$this->ignoreErrors($oldIgnore);
+ $retVal = true;
+
+ //if (in_array('IGNORE', $options))
+ // $this->ignoreErrors($oldIgnore);
+
+ return $retVal;
+ }
+
+ function insertOneRow($table, $row, $fname) {
+ // "INSERT INTO tables (a, b, c)"
+ $sql = "INSERT INTO " . $this->tableName($table) . " (" . join(',', array_keys($row)) . ')';
+ $sql .= " VALUES (";
+
+ // for each value, append ":key"
+ $first = true;
+ $returning = '';
+ foreach ($row as $col => $val) {
+ if (is_object($val)) {
+ $what = "EMPTY_BLOB()";
+ assert($returning === '');
+ $returning = " RETURNING $col INTO :bval";
+ $blobcol = $col;
+ } else
+ $what = ":$col";
+
+ if ($first)
+ $sql .= "$what";
+ else
+ $sql.= ", $what";
+ $first = false;
+ }
+ $sql .= ") $returning";
+
+ $stmt = oci_parse($this->mConn, $sql);
+ foreach ($row as $col => $val) {
+ if (!is_object($val)) {
+ if (oci_bind_by_name($stmt, ":$col", $row[$col]) === false)
+ $this->reportQueryError($this->lastErrno(), $this->lastError(), $sql, __METHOD__);
+ }
+ }
+
+ if (($bval = oci_new_descriptor($this->mConn, OCI_D_LOB)) === false) {
+ $e = oci_error($stmt);
+ throw new DBUnexpectedError($this, "Cannot create LOB descriptor: " . $e['message']);
+ }
+
+ if (strlen($returning))
+ oci_bind_by_name($stmt, ":bval", $bval, -1, SQLT_BLOB);
+
+ if (oci_execute($stmt, OCI_DEFAULT) === false) {
+ $e = oci_error($stmt);
+ $this->reportQueryError($e['message'], $e['code'], $sql, __METHOD__);
+ }
+ if (strlen($returning)) {
+ $bval->save($row[$blobcol]->getData());
+ $bval->free();
+ }
+ if (!$this->mTrxLevel)
+ oci_commit($this->mConn);
+
+ oci_free_statement($stmt);
+ }
+
+ function tableName( $name ) {
+ # Replace reserved words with better ones
+ switch( $name ) {
+ case 'user':
+ return 'mwuser';
+ case 'text':
+ return 'pagecontent';
+ default:
+ return $name;
+ }
+ }
+
+ /**
+ * Return the next in a sequence, save the value for retrieval via insertId()
+ */
+ function nextSequenceValue($seqName) {
+ $res = $this->query("SELECT $seqName.nextval FROM dual");
+ $row = $this->fetchRow($res);
+ $this->mInsertId = $row[0];
+ $this->freeResult($res);
+ return $this->mInsertId;
+ }
+
+ /**
+ * Oracle does not have a "USE INDEX" clause, so return an empty string
+ */
+ function useIndexClause($index) {
+ return '';
+ }
+
+ # REPLACE query wrapper
+ # Oracle simulates this with a DELETE followed by INSERT
+ # $row is the row to insert, an associative array
+ # $uniqueIndexes is an array of indexes. Each element may be either a
+ # field name or an array of field names
+ #
+ # It may be more efficient to leave off unique indexes which are unlikely to collide.
+ # However if you do this, you run the risk of encountering errors which wouldn't have
+ # occurred in MySQL
+ function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) {
+ $table = $this->tableName($table);
+
+ if (count($rows)==0) {
+ return;
+ }
+
+ # Single row case
+ if (!is_array(reset($rows))) {
+ $rows = array($rows);
+ }
+
+ foreach( $rows as $row ) {
+ # Delete rows which collide
+ if ( $uniqueIndexes ) {
+ $sql = "DELETE FROM $table WHERE ";
+ $first = true;
+ foreach ( $uniqueIndexes as $index ) {
+ if ( $first ) {
+ $first = false;
+ $sql .= "(";
+ } else {
+ $sql .= ') OR (';
+ }
+ if ( is_array( $index ) ) {
+ $first2 = true;
+ foreach ( $index as $col ) {
+ if ( $first2 ) {
+ $first2 = false;
+ } else {
+ $sql .= ' AND ';
+ }
+ $sql .= $col.'=' . $this->addQuotes( $row[$col] );
+ }
+ } else {
+ $sql .= $index.'=' . $this->addQuotes( $row[$index] );
+ }
+ }
+ $sql .= ')';
+ $this->query( $sql, $fname );
+ }
+
+ # Now insert the row
+ $sql = "INSERT INTO $table (" . $this->makeList( array_keys( $row ), LIST_NAMES ) .') VALUES (' .
+ $this->makeList( $row, LIST_COMMA ) . ')';
+ $this->query($sql, $fname);
+ }
+ }
+
+ # DELETE where the condition is a join
+ function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "Database::deleteJoin" ) {
+ if ( !$conds ) {
+ throw new DBUnexpectedError($this, 'Database::deleteJoin() called with empty $conds' );
+ }
+
+ $delTable = $this->tableName( $delTable );
+ $joinTable = $this->tableName( $joinTable );
+ $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
+ if ( $conds != '*' ) {
+ $sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND );
+ }
+ $sql .= ')';
+
+ $this->query( $sql, $fname );
+ }
+
+ # Returns the size of a text field, or -1 for "unlimited"
+ function textFieldSize( $table, $field ) {
+ $table = $this->tableName( $table );
+ $sql = "SELECT t.typname as ftype,a.atttypmod as size
+ FROM pg_class c, pg_attribute a, pg_type t
+ WHERE relname='$table' AND a.attrelid=c.oid AND
+ a.atttypid=t.oid and a.attname='$field'";
+ $res =$this->query($sql);
+ $row=$this->fetchObject($res);
+ if ($row->ftype=="varchar") {
+ $size=$row->size-4;
+ } else {
+ $size=$row->size;
+ }
+ $this->freeResult( $res );
+ return $size;
+ }
+
+ function lowPriorityOption() {
+ return '';
+ }
+
+ function limitResult($sql, $limit, $offset) {
+ if ($offset === false)
+ $offset = 0;
+ return "SELECT * FROM ($sql) WHERE rownum >= (1 + $offset) AND rownum < 1 + $limit + $offset";
+ }
+
+ /**
+ * Returns an SQL expression for a simple conditional.
+ * Uses CASE on 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
+ */
+ function conditional( $cond, $trueVal, $falseVal ) {
+ return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
+ }
+
+ function wasDeadlock() {
+ return $this->lastErrno() == 'OCI-00060';
+ }
+
+ function timestamp($ts = 0) {
+ return wfTimestamp(TS_ORACLE, $ts);
+ }
+
+ /**
+ * Return aggregated value function call
+ */
+ function aggregateValue ($valuedata,$valuename='value') {
+ return $valuedata;
+ }
+
+ function reportQueryError($error, $errno, $sql, $fname, $tempIgnore = false) {
+ # Ignore errors during error handling to avoid infinite
+ # recursion
+ $ignore = $this->ignoreErrors(true);
+ ++$this->mErrorCount;
+
+ if ($ignore || $tempIgnore) {
+echo "error ignored! query = [$sql]\n";
+ wfDebug("SQL ERROR (ignored): $error\n");
+ $this->ignoreErrors( $ignore );
+ }
+ else {
+echo "error!\n";
+ $message = "A database error has occurred\n" .
+ "Query: $sql\n" .
+ "Function: $fname\n" .
+ "Error: $errno $error\n";
+ throw new DBUnexpectedError($this, $message);
+ }
+ }
+
+ /**
+ * @return string wikitext of a link to the server software's web site
+ */
+ function getSoftwareLink() {
+ return "[http://www.oracle.com/ Oracle]";
+ }
+
+ /**
+ * @return string Version information from the database
+ */
+ function getServerVersion() {
+ return oci_server_version($this->mConn);
+ }
+
+ /**
+ * Query whether a given table exists (in the given schema, or the default mw one if not given)
+ */
+ function tableExists($table) {
+ $etable= $this->addQuotes($table);
+ $SQL = "SELECT 1 FROM user_tables WHERE table_name='$etable'";
+ $res = $this->query($SQL);
+ $count = $res ? oci_num_rows($res) : 0;
+ if ($res)
+ $this->freeResult($res);
+ return $count;
+ }
+
+ /**
+ * Query whether a given column exists in the mediawiki schema
+ */
+ function fieldExists( $table, $field ) {
+ return true; // XXX
+ }
+
+ function fieldInfo( $table, $field ) {
+ return false; // XXX
+ }
+
+ function begin( $fname = '' ) {
+ $this->mTrxLevel = 1;
+ }
+ function immediateCommit( $fname = '' ) {
+ return true;
+ }
+ function commit( $fname = '' ) {
+ oci_commit($this->mConn);
+ $this->mTrxLevel = 0;
+ }
+
+ /* Not even sure why this is used in the main codebase... */
+ function limitResultForUpdate($sql, $num) {
+ return $sql;
+ }
+
+ function strencode($s) {
+ return str_replace("'", "''", $s);
+ }
+
+ function encodeBlob($b) {
+ return new ORABlob($b);
+ }
+ function decodeBlob($b) {
+ return $b; //return $b->load();
+ }
+
+ function addQuotes( $s ) {
+ global $wgLang;
+ $s = $wgLang->checkTitleEncoding($s);
+ return "'" . $this->strencode($s) . "'";
+ }
+
+ function quote_ident( $s ) {
+ return $s;
+ }
+
+ /* For now, does nothing */
+ function selectDB( $db ) {
+ return true;
+ }
+
+ /**
+ * Returns an optional USE INDEX clause to go after the table, and a
+ * string to go at the end of the query
+ *
+ * @private
+ *
+ * @param array $options an associative array of options to be turned into
+ * an SQL query, valid keys are listed in the function.
+ * @return array
+ */
+ function makeSelectOptions( $options ) {
+ $preLimitTail = $postLimitTail = '';
+ $startOpts = '';
+
+ $noKeyOptions = array();
+ foreach ( $options as $key => $option ) {
+ if ( is_numeric( $key ) ) {
+ $noKeyOptions[$option] = true;
+ }
+ }
+
+ if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY {$options['GROUP BY']}";
+ if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY {$options['ORDER BY']}";
+
+ if (isset($options['LIMIT'])) {
+ // $tailOpts .= $this->limitResult('', $options['LIMIT'],
+ // isset($options['OFFSET']) ? $options['OFFSET']
+ // : false);
+ }
+
+ #if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $tailOpts .= ' FOR UPDATE';
+ #if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $tailOpts .= ' LOCK IN SHARE MODE';
+ if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
+
+ if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) {
+ $useIndex = $this->useIndexClause( $options['USE INDEX'] );
+ } else {
+ $useIndex = '';
+ }
+
+ return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
+ }
+
+ public function setTimeout( $timeout ) {
+ // @todo fixme no-op
+ }
+
+ function ping() {
+ wfDebug( "Function ping() not written for DatabaseOracle.php yet");
+ return true;
+ }
+
+ /**
+ * How lagged is this slave?
+ *
+ * @return int
+ */
+ public function getLag() {
+ # Not implemented for Oracle
+ return 0;
+ }
+
+ function setFakeSlaveLag( $lag ) {}
+ function setFakeMaster( $enabled = true ) {}
+
+ function getDBname() {
+ return $this->mDBname;
+ }
+
+ function getServer() {
+ return $this->mServer;
+ }
+
+ /**
+ * No-op lock functions
+ */
+ public function lock( $lockName, $method ) {
+ return true;
+ }
+ public function unlock( $lockName, $method ) {
+ return true;
+ }
+
+} // end DatabaseOracle class
diff --git a/includes/db/DatabasePostgres.php b/includes/db/DatabasePostgres.php
new file mode 100644
index 00000000..065ad56d
--- /dev/null
+++ b/includes/db/DatabasePostgres.php
@@ -0,0 +1,1394 @@
+<?php
+/**
+ * @ingroup Database
+ * @file
+ * This is the Postgres database abstraction layer.
+ *
+ */
+class PostgresField {
+ private $name, $tablename, $type, $nullable, $max_length;
+
+ static function fromText($db, $table, $field) {
+ global $wgDBmwschema;
+
+ $q = <<<END
+SELECT
+CASE WHEN typname = 'int2' THEN 'smallint'
+WHEN typname = 'int4' THEN 'integer'
+WHEN typname = 'int8' THEN 'bigint'
+WHEN typname = 'bpchar' THEN 'char'
+ELSE typname END AS typname,
+attnotnull, attlen
+FROM pg_class, pg_namespace, pg_attribute, pg_type
+WHERE relnamespace=pg_namespace.oid
+AND relkind='r'
+AND attrelid=pg_class.oid
+AND atttypid=pg_type.oid
+AND nspname=%s
+AND relname=%s
+AND attname=%s;
+END;
+ $res = $db->query(sprintf($q,
+ $db->addQuotes($wgDBmwschema),
+ $db->addQuotes($table),
+ $db->addQuotes($field)));
+ $row = $db->fetchObject($res);
+ if (!$row)
+ return null;
+ $n = new PostgresField;
+ $n->type = $row->typname;
+ $n->nullable = ($row->attnotnull == 'f');
+ $n->name = $field;
+ $n->tablename = $table;
+ $n->max_length = $row->attlen;
+ return $n;
+ }
+
+ function name() {
+ return $this->name;
+ }
+
+ function tableName() {
+ return $this->tablename;
+ }
+
+ function type() {
+ return $this->type;
+ }
+
+ function nullable() {
+ return $this->nullable;
+ }
+
+ function maxLength() {
+ return $this->max_length;
+ }
+}
+
+/**
+ * @ingroup Database
+ */
+class DatabasePostgres extends Database {
+ var $mInsertId = NULL;
+ var $mLastResult = NULL;
+ var $numeric_version = NULL;
+
+ function DatabasePostgres($server = false, $user = false, $password = false, $dbName = false,
+ $failFunction = false, $flags = 0 )
+ {
+
+ global $wgOut;
+ # Can't get a reference if it hasn't been set yet
+ if ( !isset( $wgOut ) ) {
+ $wgOut = NULL;
+ }
+ $this->mOut =& $wgOut;
+ $this->mFailFunction = $failFunction;
+ $this->mFlags = $flags;
+ $this->open( $server, $user, $password, $dbName);
+
+ }
+
+ function cascadingDeletes() {
+ return true;
+ }
+ function cleanupTriggers() {
+ return true;
+ }
+ function strictIPs() {
+ return true;
+ }
+ function realTimestamps() {
+ return true;
+ }
+ function implicitGroupby() {
+ return false;
+ }
+ function implicitOrderby() {
+ return false;
+ }
+ function searchableIPs() {
+ return true;
+ }
+ function functionalIndexes() {
+ return true;
+ }
+
+ function hasConstraint( $name ) {
+ global $wgDBmwschema;
+ $SQL = "SELECT 1 FROM pg_catalog.pg_constraint c, pg_catalog.pg_namespace n WHERE c.connamespace = n.oid AND conname = '" . pg_escape_string( $name ) . "' AND n.nspname = '" . pg_escape_string($wgDBmwschema) ."'";
+ return $this->numRows($res = $this->doQuery($SQL));
+ }
+
+ static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0)
+ {
+ return new DatabasePostgres( $server, $user, $password, $dbName, $failFunction, $flags );
+ }
+
+ /**
+ * Usually aborts on failure
+ * If the failFunction is set to a non-zero integer, returns success
+ */
+ function open( $server, $user, $password, $dbName ) {
+ # Test for Postgres support, to avoid suppressed fatal error
+ if ( !function_exists( 'pg_connect' ) ) {
+ throw new DBConnectionError( $this, "Postgres functions missing, have you compiled PHP with the --with-pgsql option?\n (Note: if you recently installed PHP, you may need to restart your webserver and database)\n" );
+ }
+
+ global $wgDBport;
+
+ if (!strlen($user)) { ## e.g. the class is being loaded
+ return;
+ }
+
+ $this->close();
+ $this->mServer = $server;
+ $this->mPort = $port = $wgDBport;
+ $this->mUser = $user;
+ $this->mPassword = $password;
+ $this->mDBname = $dbName;
+
+ $hstring="";
+ if ($server!=false && $server!="") {
+ $hstring="host=$server ";
+ }
+ if ($port!=false && $port!="") {
+ $hstring .= "port=$port ";
+ }
+
+ error_reporting( E_ALL );
+ @$this->mConn = pg_connect("$hstring dbname=$dbName user=$user password=$password");
+
+ if ( $this->mConn == false ) {
+ wfDebug( "DB connection error\n" );
+ wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" );
+ wfDebug( $this->lastError()."\n" );
+ return false;
+ }
+
+ $this->mOpened = true;
+
+ global $wgCommandLineMode;
+ ## If called from the command-line (e.g. importDump), only show errors
+ if ($wgCommandLineMode) {
+ $this->doQuery("SET client_min_messages = 'ERROR'");
+ }
+
+ global $wgDBmwschema, $wgDBts2schema;
+ if (isset( $wgDBmwschema ) && isset( $wgDBts2schema )
+ && $wgDBmwschema !== 'mediawiki'
+ && preg_match( '/^\w+$/', $wgDBmwschema )
+ && preg_match( '/^\w+$/', $wgDBts2schema )
+ ) {
+ $safeschema = $this->quote_ident($wgDBmwschema);
+ $safeschema2 = $this->quote_ident($wgDBts2schema);
+ $this->doQuery("SET search_path = $safeschema, $wgDBts2schema, public");
+ }
+
+ return $this->mConn;
+ }
+
+
+ function initial_setup($password, $dbName) {
+ // If this is the initial connection, setup the schema stuff and possibly create the user
+ global $wgDBname, $wgDBuser, $wgDBpassword, $wgDBsuperuser, $wgDBmwschema, $wgDBts2schema;
+
+ 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 $this->numeric_version ($version)</li>\n";
+ dieout("</ul>");
+ }
+ print "version $this->numeric_version is OK.</li>\n";
+
+ $safeuser = $this->quote_ident($wgDBuser);
+ // Are we connecting as a superuser for the first time?
+ if ($wgDBsuperuser) {
+ // Are we really a superuser? Check out our rights
+ $SQL = "SELECT
+ CASE WHEN usesuper IS TRUE THEN
+ CASE WHEN usecreatedb IS TRUE THEN 3 ELSE 1 END
+ ELSE CASE WHEN usecreatedb IS TRUE THEN 2 ELSE 0 END
+ END AS rights
+ FROM pg_catalog.pg_user WHERE usename = " . $this->addQuotes($wgDBsuperuser);
+ $rows = $this->numRows($res = $this->doQuery($SQL));
+ if (!$rows) {
+ print "<li>ERROR: Could not read permissions for user \"$wgDBsuperuser\"</li>\n";
+ dieout('</ul>');
+ }
+ $perms = pg_fetch_result($res, 0, 0);
+
+ $SQL = "SELECT 1 FROM pg_catalog.pg_user WHERE usename = " . $this->addQuotes($wgDBuser);
+ $rows = $this->numRows($this->doQuery($SQL));
+ if ($rows) {
+ print "<li>User \"$wgDBuser\" already exists, skipping account creation.</li>";
+ }
+ else {
+ if ($perms != 1 and $perms != 3) {
+ print "<li>ERROR: the user \"$wgDBsuperuser\" cannot create other users. ";
+ print 'Please use a different Postgres user.</li>';
+ dieout('</ul>');
+ }
+ print "<li>Creating user <b>$wgDBuser</b>...";
+ $safepass = $this->addQuotes($wgDBpassword);
+ $SQL = "CREATE USER $safeuser NOCREATEDB PASSWORD $safepass";
+ $this->doQuery($SQL);
+ print "OK</li>\n";
+ }
+ // User now exists, check out the database
+ if ($dbName != $wgDBname) {
+ $SQL = "SELECT 1 FROM pg_catalog.pg_database WHERE datname = " . $this->addQuotes($wgDBname);
+ $rows = $this->numRows($this->doQuery($SQL));
+ if ($rows) {
+ print "<li>Database \"$wgDBname\" already exists, skipping database creation.</li>";
+ }
+ else {
+ if ($perms < 2) {
+ print "<li>ERROR: the user \"$wgDBsuperuser\" cannot create databases. ";
+ print 'Please use a different Postgres user.</li>';
+ dieout('</ul>');
+ }
+ print "<li>Creating database <b>$wgDBname</b>...";
+ $safename = $this->quote_ident($wgDBname);
+ $SQL = "CREATE DATABASE $safename OWNER $safeuser ";
+ $this->doQuery($SQL);
+ print "OK</li>\n";
+ // Hopefully tsearch2 and plpgsql are in template1...
+ }
+
+ // Reconnect to check out tsearch2 rights for this user
+ print "<li>Connecting to \"$wgDBname\" as superuser \"$wgDBsuperuser\" to check rights...";
+
+ $hstring="";
+ if ($this->mServer!=false && $this->mServer!="") {
+ $hstring="host=$this->mServer ";
+ }
+ if ($this->mPort!=false && $this->mPort!="") {
+ $hstring .= "port=$this->mPort ";
+ }
+
+ @$this->mConn = pg_connect("$hstring dbname=$wgDBname user=$wgDBsuperuser password=$password");
+ if ( $this->mConn == false ) {
+ print "<b>FAILED TO CONNECT!</b></li>";
+ dieout("</ul>");
+ }
+ print "OK</li>\n";
+ }
+
+ if ($this->numeric_version < 8.3) {
+ // Tsearch2 checks
+ print "<li>Checking that tsearch2 is installed in the database \"$wgDBname\"...";
+ if (! $this->tableExists("pg_ts_cfg", $wgDBts2schema)) {
+ print "<b>FAILED</b>. tsearch2 must be installed in the database \"$wgDBname\".";
+ print "Please see <a href='http://www.devx.com/opensource/Article/21674/0/page/2'>this article</a>";
+ print " for instructions or ask on #postgresql on irc.freenode.net</li>\n";
+ dieout("</ul>");
+ }
+ print "OK</li>\n";
+ print "<li>Ensuring that user \"$wgDBuser\" has select rights on the tsearch2 tables...";
+ foreach (array('cfg','cfgmap','dict','parser') as $table) {
+ $SQL = "GRANT SELECT ON pg_ts_$table TO $safeuser";
+ $this->doQuery($SQL);
+ }
+ print "OK</li>\n";
+ }
+
+ // Setup the schema for this user if needed
+ $result = $this->schemaExists($wgDBmwschema);
+ $safeschema = $this->quote_ident($wgDBmwschema);
+ if (!$result) {
+ print "<li>Creating schema <b>$wgDBmwschema</b> ...";
+ $result = $this->doQuery("CREATE SCHEMA $safeschema AUTHORIZATION $safeuser");
+ if (!$result) {
+ print "<b>FAILED</b>.</li>\n";
+ dieout("</ul>");
+ }
+ print "OK</li>\n";
+ }
+ else {
+ print "<li>Schema already exists, explicitly granting rights...\n";
+ $safeschema2 = $this->addQuotes($wgDBmwschema);
+ $SQL = "SELECT 'GRANT ALL ON '||pg_catalog.quote_ident(relname)||' TO $safeuser;'\n".
+ "FROM pg_catalog.pg_class p, pg_catalog.pg_namespace n\n".
+ "WHERE relnamespace = n.oid AND n.nspname = $safeschema2\n".
+ "AND p.relkind IN ('r','S','v')\n";
+ $SQL .= "UNION\n";
+ $SQL .= "SELECT 'GRANT ALL ON FUNCTION '||pg_catalog.quote_ident(proname)||'('||\n".
+ "pg_catalog.oidvectortypes(p.proargtypes)||') TO $safeuser;'\n".
+ "FROM pg_catalog.pg_proc p, pg_catalog.pg_namespace n\n".
+ "WHERE p.pronamespace = n.oid AND n.nspname = $safeschema2";
+ $res = $this->doQuery($SQL);
+ if (!$res) {
+ print "<b>FAILED</b>. Could not set rights for the user.</li>\n";
+ dieout("</ul>");
+ }
+ $this->doQuery("SET search_path = $safeschema");
+ $rows = $this->numRows($res);
+ while ($rows) {
+ $rows--;
+ $this->doQuery(pg_fetch_result($res, $rows, 0));
+ }
+ print "OK</li>";
+ }
+
+ // Install plpgsql if needed
+ $this->setup_plpgsql();
+
+ $wgDBsuperuser = '';
+ return true; // Reconnect as regular user
+
+ } // end superuser
+
+ if (!defined('POSTGRES_SEARCHPATH')) {
+
+ if ($this->numeric_version < 8.3) {
+ // Do we have the basic tsearch2 table?
+ print "<li>Checking for tsearch2 in the schema \"$wgDBts2schema\"...";
+ if (! $this->tableExists("pg_ts_dict", $wgDBts2schema)) {
+ print "<b>FAILED</b>. Make sure tsearch2 is installed. See <a href=";
+ print "'http://www.devx.com/opensource/Article/21674/0/page/2'>this article</a>";
+ print " for instructions.</li>\n";
+ dieout("</ul>");
+ }
+ print "OK</li>\n";
+
+ // Does this user have the rights to the tsearch2 tables?
+ $ctype = pg_fetch_result($this->doQuery("SHOW lc_ctype"),0,0);
+ print "<li>Checking tsearch2 permissions...";
+ // Let's check all four, just to be safe
+ error_reporting( 0 );
+ $ts2tables = array('cfg','cfgmap','dict','parser');
+ $safetsschema = $this->quote_ident($wgDBts2schema);
+ foreach ( $ts2tables AS $tname ) {
+ $SQL = "SELECT count(*) FROM $safetsschema.pg_ts_$tname";
+ $res = $this->doQuery($SQL);
+ if (!$res) {
+ print "<b>FAILED</b> to access pg_ts_$tname. Make sure that the user ".
+ "\"$wgDBuser\" has SELECT access to all four tsearch2 tables</li>\n";
+ dieout("</ul>");
+ }
+ }
+ $SQL = "SELECT ts_name FROM $safetsschema.pg_ts_cfg WHERE locale = '$ctype'";
+ $SQL .= " ORDER BY CASE WHEN ts_name <> 'default' THEN 1 ELSE 0 END";
+ $res = $this->doQuery($SQL);
+ error_reporting( E_ALL );
+ if (!$res) {
+ print "<b>FAILED</b>. Could not determine the tsearch2 locale information</li>\n";
+ dieout("</ul>");
+ }
+ print "OK</li>";
+
+ // Will the current locale work? Can we force it to?
+ print "<li>Verifying tsearch2 locale with $ctype...";
+ $rows = $this->numRows($res);
+ $resetlocale = 0;
+ if (!$rows) {
+ print "<b>not found</b></li>\n";
+ print "<li>Attempting to set default tsearch2 locale to \"$ctype\"...";
+ $resetlocale = 1;
+ }
+ else {
+ $tsname = pg_fetch_result($res, 0, 0);
+ if ($tsname != 'default') {
+ print "<b>not set to default ($tsname)</b>";
+ print "<li>Attempting to change tsearch2 default locale to \"$ctype\"...";
+ $resetlocale = 1;
+ }
+ }
+ if ($resetlocale) {
+ $SQL = "UPDATE $safetsschema.pg_ts_cfg SET locale = '$ctype' WHERE ts_name = 'default'";
+ $res = $this->doQuery($SQL);
+ if (!$res) {
+ print "<b>FAILED</b>. ";
+ print "Please make sure that the locale in pg_ts_cfg for \"default\" is set to \"$ctype\"</li>\n";
+ dieout("</ul>");
+ }
+ print "OK</li>";
+ }
+
+ // Final test: try out a simple tsearch2 query
+ $SQL = "SELECT $safetsschema.to_tsvector('default','MediaWiki tsearch2 testing')";
+ $res = $this->doQuery($SQL);
+ if (!$res) {
+ print "<b>FAILED</b>. Specifically, \"$SQL\" did not work.</li>";
+ dieout("</ul>");
+ }
+ print "OK</li>";
+ }
+
+ // Install plpgsql if needed
+ $this->setup_plpgsql();
+
+ // Does the schema already exist? Who owns it?
+ $result = $this->schemaExists($wgDBmwschema);
+ if (!$result) {
+ print "<li>Creating schema <b>$wgDBmwschema</b> ...";
+ error_reporting( 0 );
+ $safeschema = $this->quote_ident($wgDBmwschema);
+ $result = $this->doQuery("CREATE SCHEMA $safeschema");
+ error_reporting( E_ALL );
+ if (!$result) {
+ print "<b>FAILED</b>. The user \"$wgDBuser\" must be able to access the schema. ".
+ "You can try making them the owner of the database, or try creating the schema with a ".
+ "different user, and then grant access to the \"$wgDBuser\" user.</li>\n";
+ dieout("</ul>");
+ }
+ print "OK</li>\n";
+ }
+ else if ($result != $wgDBuser) {
+ print "<li>Schema \"$wgDBmwschema\" exists but is not owned by \"$wgDBuser\". Not ideal.</li>\n";
+ }
+ else {
+ print "<li>Schema \"$wgDBmwschema\" exists and is owned by \"$wgDBuser\". Excellent.</li>\n";
+ }
+
+ // Always return GMT time to accomodate the existing integer-based timestamp assumption
+ print "<li>Setting the timezone to GMT for user \"$wgDBuser\" ...";
+ $SQL = "ALTER USER $safeuser SET timezone = 'GMT'";
+ $result = pg_query($this->mConn, $SQL);
+ if (!$result) {
+ print "<b>FAILED</b>.</li>\n";
+ dieout("</ul>");
+ }
+ print "OK</li>\n";
+ // Set for the rest of this session
+ $SQL = "SET timezone = 'GMT'";
+ $result = pg_query($this->mConn, $SQL);
+ if (!$result) {
+ print "<li>Failed to set timezone</li>\n";
+ dieout("</ul>");
+ }
+
+ print "<li>Setting the datestyle to ISO, YMD for user \"$wgDBuser\" ...";
+ $SQL = "ALTER USER $safeuser SET datestyle = 'ISO, YMD'";
+ $result = pg_query($this->mConn, $SQL);
+ if (!$result) {
+ print "<b>FAILED</b>.</li>\n";
+ dieout("</ul>");
+ }
+ print "OK</li>\n";
+ // Set for the rest of this session
+ $SQL = "SET datestyle = 'ISO, YMD'";
+ $result = pg_query($this->mConn, $SQL);
+ if (!$result) {
+ print "<li>Failed to set datestyle</li>\n";
+ dieout("</ul>");
+ }
+
+ // Fix up the search paths if needed
+ print "<li>Setting the search path for user \"$wgDBuser\" ...";
+ $path = $this->quote_ident($wgDBmwschema);
+ if ($wgDBts2schema !== $wgDBmwschema)
+ $path .= ", ". $this->quote_ident($wgDBts2schema);
+ if ($wgDBmwschema !== 'public' and $wgDBts2schema !== 'public')
+ $path .= ", public";
+ $SQL = "ALTER USER $safeuser SET search_path = $path";
+ $result = pg_query($this->mConn, $SQL);
+ if (!$result) {
+ print "<b>FAILED</b>.</li>\n";
+ dieout("</ul>");
+ }
+ print "OK</li>\n";
+ // Set for the rest of this session
+ $SQL = "SET search_path = $path";
+ $result = pg_query($this->mConn, $SQL);
+ if (!$result) {
+ print "<li>Failed to set search_path</li>\n";
+ dieout("</ul>");
+ }
+ define( "POSTGRES_SEARCHPATH", $path );
+ }
+ }
+
+
+ function setup_plpgsql() {
+ print "<li>Checking for Pl/Pgsql ...";
+ $SQL = "SELECT 1 FROM pg_catalog.pg_language WHERE lanname = 'plpgsql'";
+ $rows = $this->numRows($this->doQuery($SQL));
+ if ($rows < 1) {
+ // plpgsql is not installed, but if we have a pg_pltemplate table, we should be able to create it
+ print "not installed. Attempting to install Pl/Pgsql ...";
+ $SQL = "SELECT 1 FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace) ".
+ "WHERE relname = 'pg_pltemplate' AND nspname='pg_catalog'";
+ $rows = $this->numRows($this->doQuery($SQL));
+ if ($rows >= 1) {
+ $olde = error_reporting(0);
+ error_reporting($olde - E_WARNING);
+ $result = $this->doQuery("CREATE LANGUAGE plpgsql");
+ error_reporting($olde);
+ if (!$result) {
+ print "<b>FAILED</b>. You need to install the language plpgsql in the database <tt>$wgDBname</tt></li>";
+ dieout("</ul>");
+ }
+ }
+ else {
+ print "<b>FAILED</b>. You need to install the language plpgsql in the database <tt>$wgDBname</tt></li>";
+ dieout("</ul>");
+ }
+ }
+ print "OK</li>\n";
+ }
+
+
+ /**
+ * Closes a database connection, if it is open
+ * Returns success, true if already closed
+ */
+ function close() {
+ $this->mOpened = false;
+ if ( $this->mConn ) {
+ return pg_close( $this->mConn );
+ } else {
+ return true;
+ }
+ }
+
+ function doQuery( $sql ) {
+ if (function_exists('mb_convert_encoding')) {
+ return $this->mLastResult=pg_query( $this->mConn , mb_convert_encoding($sql,'UTF-8') );
+ }
+ return $this->mLastResult=pg_query( $this->mConn , $sql);
+ }
+
+ function queryIgnore( $sql, $fname = '' ) {
+ return $this->query( $sql, $fname, true );
+ }
+
+ function freeResult( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ if ( !@pg_free_result( $res ) ) {
+ throw new DBUnexpectedError($this, "Unable to free Postgres result\n" );
+ }
+ }
+
+ function fetchObject( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ @$row = pg_fetch_object( $res );
+ # FIXME: HACK HACK HACK HACK debug
+
+ # TODO:
+ # hashar : not sure if the following test really trigger if the object
+ # fetching failed.
+ if( pg_last_error($this->mConn) ) {
+ throw new DBUnexpectedError($this, 'SQL error: ' . htmlspecialchars( pg_last_error($this->mConn) ) );
+ }
+ return $row;
+ }
+
+ function fetchRow( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ @$row = pg_fetch_array( $res );
+ if( pg_last_error($this->mConn) ) {
+ throw new DBUnexpectedError($this, 'SQL error: ' . htmlspecialchars( pg_last_error($this->mConn) ) );
+ }
+ return $row;
+ }
+
+ function numRows( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ @$n = pg_num_rows( $res );
+ if( pg_last_error($this->mConn) ) {
+ throw new DBUnexpectedError($this, 'SQL error: ' . htmlspecialchars( pg_last_error($this->mConn) ) );
+ }
+ return $n;
+ }
+ function numFields( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return pg_num_fields( $res );
+ }
+ function fieldName( $res, $n ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return pg_field_name( $res, $n );
+ }
+
+ /**
+ * This must be called after nextSequenceVal
+ */
+ function insertId() {
+ return $this->mInsertId;
+ }
+
+ function dataSeek( $res, $row ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return pg_result_seek( $res, $row );
+ }
+
+ function lastError() {
+ if ( $this->mConn ) {
+ return pg_last_error();
+ }
+ else {
+ return "No database connection";
+ }
+ }
+ function lastErrno() {
+ return pg_last_error() ? 1 : 0;
+ }
+
+ function affectedRows() {
+ if( !isset( $this->mLastResult ) or ! $this->mLastResult )
+ return 0;
+
+ return pg_affected_rows( $this->mLastResult );
+ }
+
+ /**
+ * Estimate rows in dataset
+ * Returns estimated count, based on EXPLAIN output
+ * This is not necessarily an accurate estimate, so use sparingly
+ * Returns -1 if count cannot be found
+ * Takes same arguments as Database::select()
+ */
+
+ function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) {
+ $options['EXPLAIN'] = true;
+ $res = $this->select( $table, $vars, $conds, $fname, $options );
+ $rows = -1;
+ if ( $res ) {
+ $row = $this->fetchRow( $res );
+ $count = array();
+ if( preg_match( '/rows=(\d+)/', $row[0], $count ) ) {
+ $rows = $count[1];
+ }
+ $this->freeResult($res);
+ }
+ return $rows;
+ }
+
+
+ /**
+ * Returns information about an index
+ * If errors are explicitly ignored, returns NULL on failure
+ */
+ function indexInfo( $table, $index, $fname = 'Database::indexExists' ) {
+ $sql = "SELECT indexname FROM pg_indexes WHERE tablename='$table'";
+ $res = $this->query( $sql, $fname );
+ if ( !$res ) {
+ return NULL;
+ }
+ while ( $row = $this->fetchObject( $res ) ) {
+ if ( $row->indexname == $index ) {
+ return $row;
+ }
+ }
+ return false;
+ }
+
+ function indexUnique ($table, $index, $fname = 'Database::indexUnique' ) {
+ $sql = "SELECT indexname FROM pg_indexes WHERE tablename='{$table}'".
+ " AND indexdef LIKE 'CREATE UNIQUE%({$index})'";
+ $res = $this->query( $sql, $fname );
+ if ( !$res )
+ return NULL;
+ while ($row = $this->fetchObject( $res ))
+ return true;
+ return false;
+
+ }
+
+ /**
+ * INSERT wrapper, inserts an array into a table
+ *
+ * $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
+ *
+ * @return bool Success of insert operation. IGNORE always returns true.
+ */
+ function insert( $table, $args, $fname = 'DatabasePostgres::insert', $options = array() ) {
+ global $wgDBversion;
+
+ if ( !count( $args ) ) {
+ return true;
+ }
+
+ $table = $this->tableName( $table );
+ if (! isset( $wgDBversion ) ) {
+ $this->getServerVersion();
+ $wgDBversion = $this->numeric_version;
+ }
+
+ if ( !is_array( $options ) )
+ $options = array( $options );
+
+ if ( isset( $args[0] ) && is_array( $args[0] ) ) {
+ $multi = true;
+ $keys = array_keys( $args[0] );
+ }
+ else {
+ $multi = false;
+ $keys = array_keys( $args );
+ }
+
+ // If IGNORE is set, we use savepoints to emulate mysql's behavior
+ $ignore = in_array( 'IGNORE', $options ) ? 'mw' : '';
+
+ // If we are not in a transaction, we need to be for savepoint trickery
+ $didbegin = 0;
+ if ( $ignore ) {
+ if (! $this->mTrxLevel) {
+ $this->begin();
+ $didbegin = 1;
+ }
+ $olde = error_reporting( 0 );
+ // For future use, we may want to track the number of actual inserts
+ // Right now, insert (all writes) simply return true/false
+ $numrowsinserted = 0;
+ }
+
+ $sql = "INSERT INTO $table (" . implode( ',', $keys ) . ') VALUES ';
+
+ if ( $multi ) {
+ if ( $wgDBversion >= 8.2 && !$ignore ) {
+ $first = true;
+ foreach ( $args as $row ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $sql .= ',';
+ }
+ $sql .= '(' . $this->makeList( $row ) . ')';
+ }
+ $res = (bool)$this->query( $sql, $fname, $ignore );
+ }
+ else {
+ $res = true;
+ $origsql = $sql;
+ foreach ( $args as $row ) {
+ $tempsql = $origsql;
+ $tempsql .= '(' . $this->makeList( $row ) . ')';
+
+ if ( $ignore ) {
+ pg_query($this->mConn, "SAVEPOINT $ignore");
+ }
+
+ $tempres = (bool)$this->query( $tempsql, $fname, $ignore );
+
+ if ( $ignore ) {
+ $bar = pg_last_error();
+ if ($bar != false) {
+ pg_query( $this->mConn, "ROLLBACK TO $ignore" );
+ }
+ else {
+ pg_query( $this->mConn, "RELEASE $ignore" );
+ $numrowsinserted++;
+ }
+ }
+
+ // If any of them fail, we fail overall for this function call
+ // Note that this will be ignored if IGNORE is set
+ if (! $tempres)
+ $res = false;
+ }
+ }
+ }
+ else {
+ // Not multi, just a lone insert
+ if ( $ignore ) {
+ pg_query($this->mConn, "SAVEPOINT $ignore");
+ }
+
+ $sql .= '(' . $this->makeList( $args ) . ')';
+ $res = (bool)$this->query( $sql, $fname, $ignore );
+
+ if ( $ignore ) {
+ $bar = pg_last_error();
+ if ($bar != false) {
+ pg_query( $this->mConn, "ROLLBACK TO $ignore" );
+ }
+ else {
+ pg_query( $this->mConn, "RELEASE $ignore" );
+ $numrowsinserted++;
+ }
+ }
+ }
+
+ if ( $ignore ) {
+ $olde = error_reporting( $olde );
+ if ($didbegin) {
+ $this->commit();
+ }
+
+ // IGNORE always returns true
+ return true;
+ }
+
+
+ return $res;
+
+ }
+
+ function tableName( $name ) {
+ # Replace reserved words with better ones
+ switch( $name ) {
+ case 'user':
+ return 'mwuser';
+ case 'text':
+ return 'pagecontent';
+ default:
+ return $name;
+ }
+ }
+
+ /**
+ * Return the next in a sequence, save the value for retrieval via insertId()
+ */
+ function nextSequenceValue( $seqName ) {
+ $safeseq = preg_replace( "/'/", "''", $seqName );
+ $res = $this->query( "SELECT nextval('$safeseq')" );
+ $row = $this->fetchRow( $res );
+ $this->mInsertId = $row[0];
+ $this->freeResult( $res );
+ return $this->mInsertId;
+ }
+
+ /**
+ * Return the current value of a sequence. Assumes it has ben nextval'ed in this session.
+ */
+ function currentSequenceValue( $seqName ) {
+ $safeseq = preg_replace( "/'/", "''", $seqName );
+ $res = $this->query( "SELECT currval('$safeseq')" );
+ $row = $this->fetchRow( $res );
+ $currval = $row[0];
+ $this->freeResult( $res );
+ return $currval;
+ }
+
+ /**
+ * Postgres does not have a "USE INDEX" clause, so return an empty string
+ */
+ function useIndexClause( $index ) {
+ return '';
+ }
+
+ # REPLACE query wrapper
+ # Postgres simulates this with a DELETE followed by INSERT
+ # $row is the row to insert, an associative array
+ # $uniqueIndexes is an array of indexes. Each element may be either a
+ # field name or an array of field names
+ #
+ # It may be more efficient to leave off unique indexes which are unlikely to collide.
+ # However if you do this, you run the risk of encountering errors which wouldn't have
+ # occurred in MySQL
+ function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) {
+ $table = $this->tableName( $table );
+
+ if (count($rows)==0) {
+ return;
+ }
+
+ # Single row case
+ if ( !is_array( reset( $rows ) ) ) {
+ $rows = array( $rows );
+ }
+
+ foreach( $rows as $row ) {
+ # Delete rows which collide
+ if ( $uniqueIndexes ) {
+ $sql = "DELETE FROM $table WHERE ";
+ $first = true;
+ foreach ( $uniqueIndexes as $index ) {
+ if ( $first ) {
+ $first = false;
+ $sql .= "(";
+ } else {
+ $sql .= ') OR (';
+ }
+ if ( is_array( $index ) ) {
+ $first2 = true;
+ foreach ( $index as $col ) {
+ if ( $first2 ) {
+ $first2 = false;
+ } else {
+ $sql .= ' AND ';
+ }
+ $sql .= $col.'=' . $this->addQuotes( $row[$col] );
+ }
+ } else {
+ $sql .= $index.'=' . $this->addQuotes( $row[$index] );
+ }
+ }
+ $sql .= ')';
+ $this->query( $sql, $fname );
+ }
+
+ # Now insert the row
+ $sql = "INSERT INTO $table (" . $this->makeList( array_keys( $row ), LIST_NAMES ) .') VALUES (' .
+ $this->makeList( $row, LIST_COMMA ) . ')';
+ $this->query( $sql, $fname );
+ }
+ }
+
+ # DELETE where the condition is a join
+ function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "Database::deleteJoin" ) {
+ if ( !$conds ) {
+ throw new DBUnexpectedError($this, 'Database::deleteJoin() called with empty $conds' );
+ }
+
+ $delTable = $this->tableName( $delTable );
+ $joinTable = $this->tableName( $joinTable );
+ $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
+ if ( $conds != '*' ) {
+ $sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND );
+ }
+ $sql .= ')';
+
+ $this->query( $sql, $fname );
+ }
+
+ # Returns the size of a text field, or -1 for "unlimited"
+ function textFieldSize( $table, $field ) {
+ $table = $this->tableName( $table );
+ $sql = "SELECT t.typname as ftype,a.atttypmod as size
+ FROM pg_class c, pg_attribute a, pg_type t
+ WHERE relname='$table' AND a.attrelid=c.oid AND
+ a.atttypid=t.oid and a.attname='$field'";
+ $res =$this->query($sql);
+ $row=$this->fetchObject($res);
+ if ($row->ftype=="varchar") {
+ $size=$row->size-4;
+ } else {
+ $size=$row->size;
+ }
+ $this->freeResult( $res );
+ return $size;
+ }
+
+ function lowPriorityOption() {
+ return '';
+ }
+
+ function limitResult($sql, $limit, $offset=false) {
+ return "$sql LIMIT $limit ".(is_numeric($offset)?" OFFSET {$offset} ":"");
+ }
+
+ /**
+ * 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
+ */
+ function conditional( $cond, $trueVal, $falseVal ) {
+ return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
+ }
+
+ function wasDeadlock() {
+ return $this->lastErrno() == '40P01';
+ }
+
+ function timestamp( $ts=0 ) {
+ return wfTimestamp(TS_POSTGRES,$ts);
+ }
+
+ /**
+ * Return aggregated value function call
+ */
+ function aggregateValue ($valuedata,$valuename='value') {
+ return $valuedata;
+ }
+
+
+ function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
+ // Ignore errors during error handling to avoid infinite recursion
+ $ignore = $this->ignoreErrors( true );
+ $this->mErrorCount++;
+
+ if ($ignore || $tempIgnore) {
+ wfDebug("SQL ERROR (ignored): $error\n");
+ $this->ignoreErrors( $ignore );
+ }
+ else {
+ $message = "A database error has occurred\n" .
+ "Query: $sql\n" .
+ "Function: $fname\n" .
+ "Error: $errno $error\n";
+ throw new DBUnexpectedError($this, $message);
+ }
+ }
+
+ /**
+ * @return string wikitext of a link to the server software's web site
+ */
+ function getSoftwareLink() {
+ return "[http://www.postgresql.org/ PostgreSQL]";
+ }
+
+ /**
+ * @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!");
+ }
+ $this->numeric_version = $thisver[1];
+ return $version;
+ }
+
+
+ /**
+ * Query whether a given relation exists (in the given schema, or the
+ * default mw one if not given)
+ */
+ function relationExists( $table, $types, $schema = false ) {
+ global $wgDBmwschema;
+ if (!is_array($types))
+ $types = array($types);
+ if (! $schema )
+ $schema = $wgDBmwschema;
+ $etable = $this->addQuotes($table);
+ $eschema = $this->addQuotes($schema);
+ $SQL = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n "
+ . "WHERE c.relnamespace = n.oid AND c.relname = $etable AND n.nspname = $eschema "
+ . "AND c.relkind IN ('" . implode("','", $types) . "')";
+ $res = $this->query( $SQL );
+ $count = $res ? $res->numRows() : 0;
+ if ($res)
+ $this->freeResult( $res );
+ return $count ? true : false;
+ }
+
+ /*
+ * For backward compatibility, this function checks both tables and
+ * views.
+ */
+ function tableExists ($table, $schema = false) {
+ return $this->relationExists($table, array('r', 'v'), $schema);
+ }
+
+ function sequenceExists ($sequence, $schema = false) {
+ return $this->relationExists($sequence, 'S', $schema);
+ }
+
+ function triggerExists($table, $trigger) {
+ global $wgDBmwschema;
+
+ $q = <<<END
+ SELECT 1 FROM pg_class, pg_namespace, pg_trigger
+ WHERE relnamespace=pg_namespace.oid AND relkind='r'
+ AND tgrelid=pg_class.oid
+ AND nspname=%s AND relname=%s AND tgname=%s
+END;
+ $res = $this->query(sprintf($q,
+ $this->addQuotes($wgDBmwschema),
+ $this->addQuotes($table),
+ $this->addQuotes($trigger)));
+ if (!$res)
+ return NULL;
+ $rows = $res->numRows();
+ $this->freeResult($res);
+ return $rows;
+ }
+
+ function ruleExists($table, $rule) {
+ global $wgDBmwschema;
+ $exists = $this->selectField("pg_rules", "rulename",
+ array( "rulename" => $rule,
+ "tablename" => $table,
+ "schemaname" => $wgDBmwschema));
+ return $exists === $rule;
+ }
+
+ function constraintExists($table, $constraint) {
+ global $wgDBmwschema;
+ $SQL = sprintf("SELECT 1 FROM information_schema.table_constraints ".
+ "WHERE constraint_schema = %s AND table_name = %s AND constraint_name = %s",
+ $this->addQuotes($wgDBmwschema),
+ $this->addQuotes($table),
+ $this->addQuotes($constraint));
+ $res = $this->query($SQL);
+ if (!$res)
+ return NULL;
+ $rows = $res->numRows();
+ $this->freeResult($res);
+ return $rows;
+ }
+
+ /**
+ * Query whether a given schema exists. Returns the name of the owner
+ */
+ function schemaExists( $schema ) {
+ $eschema = preg_replace("/'/", "''", $schema);
+ $SQL = "SELECT rolname FROM pg_catalog.pg_namespace n, pg_catalog.pg_roles r "
+ ."WHERE n.nspowner=r.oid AND n.nspname = '$eschema'";
+ $res = $this->query( $SQL );
+ if ( $res && $res->numRows() ) {
+ $row = $res->fetchObject();
+ $owner = $row->rolname;
+ } else {
+ $owner = false;
+ }
+ if ($res)
+ $this->freeResult($res);
+ return $owner;
+ }
+
+ /**
+ * Query whether a given column exists in the mediawiki schema
+ */
+ function fieldExists( $table, $field, $fname = 'DatabasePostgres::fieldExists' ) {
+ global $wgDBmwschema;
+ $etable = preg_replace("/'/", "''", $table);
+ $eschema = preg_replace("/'/", "''", $wgDBmwschema);
+ $ecol = preg_replace("/'/", "''", $field);
+ $SQL = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n, pg_catalog.pg_attribute a "
+ . "WHERE c.relnamespace = n.oid AND c.relname = '$etable' AND n.nspname = '$eschema' "
+ . "AND a.attrelid = c.oid AND a.attname = '$ecol'";
+ $res = $this->query( $SQL, $fname );
+ $count = $res ? $res->numRows() : 0;
+ if ($res)
+ $this->freeResult( $res );
+ return $count;
+ }
+
+ function fieldInfo( $table, $field ) {
+ return PostgresField::fromText($this, $table, $field);
+ }
+
+ /**
+ * pg_field_type() wrapper
+ */
+ function fieldType( $res, $index ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return pg_field_type( $res, $index );
+ }
+
+ function begin( $fname = 'DatabasePostgres::begin' ) {
+ $this->query( 'BEGIN', $fname );
+ $this->mTrxLevel = 1;
+ }
+ function immediateCommit( $fname = 'DatabasePostgres::immediateCommit' ) {
+ return true;
+ }
+ function commit( $fname = 'DatabasePostgres::commit' ) {
+ $this->query( 'COMMIT', $fname );
+ $this->mTrxLevel = 0;
+ }
+
+ /* Not even sure why this is used in the main codebase... */
+ function limitResultForUpdate($sql, $num) {
+ return $sql;
+ }
+
+ function setup_database() {
+ global $wgVersion, $wgDBmwschema, $wgDBts2schema, $wgDBport, $wgDBuser;
+
+ // Make sure that we can write to the correct schema
+ // If not, Postgres will happily and silently go to the next search_path item
+ $ctest = "mediawiki_test_table";
+ $safeschema = $this->quote_ident($wgDBmwschema);
+ if ($this->tableExists($ctest, $wgDBmwschema)) {
+ $this->doQuery("DROP TABLE $safeschema.$ctest");
+ }
+ $SQL = "CREATE TABLE $safeschema.$ctest(a int)";
+ $olde = error_reporting( 0 );
+ $res = $this->doQuery($SQL);
+ error_reporting( $olde );
+ if (!$res) {
+ print "<b>FAILED</b>. Make sure that the user \"$wgDBuser\" can write to the schema \"$wgDBmwschema\"</li>\n";
+ dieout("</ul>");
+ }
+ $this->doQuery("DROP TABLE $safeschema.$ctest");
+
+ $res = dbsource( "../maintenance/postgres/tables.sql", $this);
+
+ ## Update version information
+ $mwv = $this->addQuotes($wgVersion);
+ $pgv = $this->addQuotes($this->getServerVersion());
+ $pgu = $this->addQuotes($this->mUser);
+ $mws = $this->addQuotes($wgDBmwschema);
+ $tss = $this->addQuotes($wgDBts2schema);
+ $pgp = $this->addQuotes($wgDBport);
+ $dbn = $this->addQuotes($this->mDBname);
+ $ctype = pg_fetch_result($this->doQuery("SHOW lc_ctype"),0,0);
+
+ $SQL = "UPDATE mediawiki_version SET mw_version=$mwv, pg_version=$pgv, pg_user=$pgu, ".
+ "mw_schema = $mws, ts2_schema = $tss, pg_port=$pgp, pg_dbname=$dbn, ".
+ "ctype = '$ctype' ".
+ "WHERE type = 'Creation'";
+ $this->query($SQL);
+
+ ## Avoid the non-standard "REPLACE INTO" syntax
+ $f = fopen( "../maintenance/interwiki.sql", 'r' );
+ if ($f == false ) {
+ dieout( "<li>Could not find the interwiki.sql file");
+ }
+ ## We simply assume it is already empty as we have just created it
+ $SQL = "INSERT INTO interwiki(iw_prefix,iw_url,iw_local) VALUES ";
+ while ( ! feof( $f ) ) {
+ $line = fgets($f,1024);
+ $matches = array();
+ if (!preg_match('/^\s*(\(.+?),(\d)\)/', $line, $matches)) {
+ continue;
+ }
+ $this->query("$SQL $matches[1],$matches[2])");
+ }
+ print " (table interwiki successfully populated)...\n";
+
+ $this->doQuery("COMMIT");
+ }
+
+ function encodeBlob( $b ) {
+ return new Blob ( pg_escape_bytea( $b ) ) ;
+ }
+
+ function decodeBlob( $b ) {
+ if ($b instanceof Blob) {
+ $b = $b->fetch();
+ }
+ return pg_unescape_bytea( $b );
+ }
+
+ function strencode( $s ) { ## Should not be called by us
+ return pg_escape_string( $s );
+ }
+
+ function addQuotes( $s ) {
+ if ( is_null( $s ) ) {
+ return 'NULL';
+ } else if ($s instanceof Blob) {
+ return "'".$s->fetch($s)."'";
+ }
+ return "'" . pg_escape_string($s) . "'";
+ }
+
+ function quote_ident( $s ) {
+ return '"' . preg_replace( '/"/', '""', $s) . '"';
+ }
+
+ /* For now, does nothing */
+ function selectDB( $db ) {
+ return true;
+ }
+
+ /**
+ * Postgres specific version of replaceVars.
+ * Calls the parent version in Database.php
+ *
+ * @private
+ *
+ * @param string $com SQL string, read from a stream (usually tables.sql)
+ *
+ * @return string SQL string
+ */
+ protected function replaceVars( $ins ) {
+
+ $ins = parent::replaceVars( $ins );
+
+ if ($this->numeric_version >= 8.3) {
+ // Thanks for not providing backwards-compatibility, 8.3
+ $ins = preg_replace( "/to_tsvector\s*\(\s*'default'\s*,/", 'to_tsvector(', $ins );
+ }
+
+ if ($this->numeric_version <= 8.1) { // Our minimum version
+ $ins = str_replace( 'USING gin', 'USING gist', $ins );
+ }
+
+ return $ins;
+ }
+
+ /**
+ * Various select options
+ *
+ * @private
+ *
+ * @param array $options an associative array of options to be turned into
+ * an SQL query, valid keys are listed in the function.
+ * @return array
+ */
+ function makeSelectOptions( $options ) {
+ $preLimitTail = $postLimitTail = '';
+ $startOpts = $useIndex = '';
+
+ $noKeyOptions = array();
+ foreach ( $options as $key => $option ) {
+ if ( is_numeric( $key ) ) {
+ $noKeyOptions[$option] = true;
+ }
+ }
+
+ if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY " . $options['GROUP BY'];
+ if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}";
+ if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY " . $options['ORDER BY'];
+
+ //if (isset($options['LIMIT'])) {
+ // $tailOpts .= $this->limitResult('', $options['LIMIT'],
+ // isset($options['OFFSET']) ? $options['OFFSET']
+ // : false);
+ //}
+
+ if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $postLimitTail .= ' FOR UPDATE';
+ if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $postLimitTail .= ' LOCK IN SHARE MODE';
+ if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
+
+ return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
+ }
+
+ public function setTimeout( $timeout ) {
+ // @todo fixme no-op
+ }
+
+ function ping() {
+ wfDebug( "Function ping() not written for DatabasePostgres.php yet");
+ return true;
+ }
+
+ /**
+ * How lagged is this slave?
+ *
+ */
+ public function getLag() {
+ # Not implemented for PostgreSQL
+ return false;
+ }
+
+ function setFakeSlaveLag( $lag ) {}
+ function setFakeMaster( $enabled = true ) {}
+
+ function getDBname() {
+ return $this->mDBname;
+ }
+
+ function getServer() {
+ return $this->mServer;
+ }
+
+ function buildConcat( $stringList ) {
+ return implode( ' || ', $stringList );
+ }
+
+ /* These are not used yet, but we know we don't want the default version */
+
+ public function lock( $lockName, $method ) {
+ return true;
+ }
+ public function unlock( $lockName, $method ) {
+ return true;
+ }
+
+} // end DatabasePostgres class
diff --git a/includes/db/DatabaseSqlite.php b/includes/db/DatabaseSqlite.php
new file mode 100644
index 00000000..5299c688
--- /dev/null
+++ b/includes/db/DatabaseSqlite.php
@@ -0,0 +1,405 @@
+<?php
+/**
+ * This script is the SQLite database abstraction layer
+ *
+ * See maintenance/sqlite/README for development notes and other specific information
+ * @ingroup Database
+ * @file
+ */
+
+/**
+ * @ingroup Database
+ */
+class DatabaseSqlite extends Database {
+
+ var $mAffectedRows;
+ var $mLastResult;
+ var $mDatabaseFile;
+
+ /**
+ * Constructor
+ */
+ function __construct($server = false, $user = false, $password = false, $dbName = false, $failFunction = false, $flags = 0) {
+ global $wgOut,$wgSQLiteDataDir;
+ 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;
+ $this->mFailFunction = $failFunction;
+ $this->mFlags = $flags;
+ $this->mDatabaseFile = "$wgSQLiteDataDir/$dbName.sqlite";
+ $this->open($server, $user, $password, $dbName);
+ }
+
+ /**
+ * todo: check if these should be true like parent class
+ */
+ function implicitGroupby() { return false; }
+ function implicitOrderby() { return false; }
+
+ static function newFromParams($server, $user, $password, $dbName, $failFunction = false, $flags = 0) {
+ return new DatabaseSqlite($server, $user, $password, $dbName, $failFunction, $flags);
+ }
+
+ /** Open an SQLite database and return a resource handle to it
+ * NOTE: only $dbName is used, the other parameters are irrelevant for SQLite databases
+ */
+ function open($server,$user,$pass,$dbName) {
+ $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");;
+ $this->mOpened = $this->mConn;
+ $this->mConn->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_SILENT); # set error codes only, dont raise exceptions
+ }
+ return $this->mConn;
+ }
+
+ /**
+ * Close an SQLite database
+ */
+ function close() {
+ $this->mOpened = false;
+ if (is_object($this->mConn)) {
+ if ($this->trxLevel()) $this->immediateCommit();
+ $this->mConn = null;
+ }
+ return true;
+ }
+
+ /**
+ * SQLite doesn't allow buffered results or data seeking etc, so we'll use fetchAll as the result
+ */
+ function doQuery($sql) {
+ $res = $this->mConn->query($sql);
+ if ($res === false) $this->reportQueryError($this->lastError(),$this->lastErrno(),$sql,__FUNCTION__);
+ else {
+ $r = $res instanceof ResultWrapper ? $res->result : $res;
+ $this->mAffectedRows = $r->rowCount();
+ $res = new ResultWrapper($this,$r->fetchAll());
+ }
+ return $res;
+ }
+
+ function freeResult(&$res) {
+ if ($res instanceof ResultWrapper) $res->result = NULL; else $res = NULL;
+ }
+
+ function fetchObject(&$res) {
+ if ($res instanceof ResultWrapper) $r =& $res->result; else $r =& $res;
+ $cur = current($r);
+ if (is_array($cur)) {
+ next($r);
+ $obj = new stdClass;
+ foreach ($cur as $k => $v) if (!is_numeric($k)) $obj->$k = $v;
+ return $obj;
+ }
+ return false;
+ }
+
+ function fetchRow(&$res) {
+ if ($res instanceof ResultWrapper) $r =& $res->result; else $r =& $res;
+ $cur = current($r);
+ if (is_array($cur)) {
+ next($r);
+ return $cur;
+ }
+ return false;
+ }
+
+ /**
+ * The PDO::Statement class implements the array interface so count() will work
+ */
+ function numRows(&$res) {
+ $r = $res instanceof ResultWrapper ? $res->result : $res;
+ return count($r);
+ }
+
+ function numFields(&$res) {
+ $r = $res instanceof ResultWrapper ? $res->result : $res;
+ return is_array($r) ? count($r[0]) : 0;
+ }
+
+ function fieldName(&$res,$n) {
+ $r = $res instanceof ResultWrapper ? $res->result : $res;
+ if (is_array($r)) {
+ $keys = array_keys($r[0]);
+ return $keys[$n];
+ }
+ return false;
+ }
+
+ /**
+ * Use MySQL's naming (accounts for prefix etc) but remove surrounding backticks
+ */
+ function tableName($name) {
+ return str_replace('`','',parent::tableName($name));
+ }
+
+ /**
+ * This must be called after nextSequenceVal
+ */
+ function insertId() {
+ return $this->mConn->lastInsertId();
+ }
+
+ function dataSeek(&$res,$row) {
+ if ($res instanceof ResultWrapper) $r =& $res->result; else $r =& $res;
+ reset($r);
+ if ($row > 0) for ($i = 0; $i < $row; $i++) next($r);
+ }
+
+ function lastError() {
+ if (!is_object($this->mConn)) return "Cannot return last error, no db connection";
+ $e = $this->mConn->errorInfo();
+ return isset($e[2]) ? $e[2] : '';
+ }
+
+ function lastErrno() {
+ if (!is_object($this->mConn)) return "Cannot return last error, no db connection";
+ return $this->mConn->errorCode();
+ }
+
+ function affectedRows() {
+ return $this->mAffectedRows;
+ }
+
+ /**
+ * Returns information about an index
+ * - if errors are explicitly ignored, returns NULL on failure
+ */
+ function indexInfo($table, $index, $fname = 'Database::indexExists') {
+ return false;
+ }
+
+ function indexUnique($table, $index, $fname = 'Database::indexUnique') {
+ return false;
+ }
+
+ /**
+ * Filter the options used in SELECT statements
+ */
+ function makeSelectOptions($options) {
+ foreach ($options as $k => $v) if (is_numeric($k) && $v == 'FOR UPDATE') $options[$k] = '';
+ return parent::makeSelectOptions($options);
+ }
+
+ /**
+ * Based on MySQL method (parent) with some prior SQLite-sepcific adjustments
+ */
+ function insert($table, $a, $fname = 'DatabaseSqlite::insert', $options = array()) {
+ if (!count($a)) return true;
+ if (!is_array($options)) $options = array($options);
+
+ # SQLite uses OR IGNORE not just IGNORE
+ foreach ($options as $k => $v) if ($v == 'IGNORE') $options[$k] = 'OR IGNORE';
+
+ # SQLite can't handle multi-row inserts, so divide up into multiple single-row inserts
+ if (isset($a[0]) && is_array($a[0])) {
+ $ret = true;
+ foreach ($a as $k => $v) if (!parent::insert($table,$v,"$fname/multi-row",$options)) $ret = false;
+ }
+ else $ret = parent::insert($table,$a,"$fname/single-row",$options);
+
+ return $ret;
+ }
+
+ /**
+ * SQLite does not have a "USE INDEX" clause, so return an empty string
+ */
+ function useIndexClause($index) {
+ return '';
+ }
+
+ # Returns the size of a text field, or -1 for "unlimited"
+ function textFieldSize($table, $field) {
+ return -1;
+ }
+
+ /**
+ * No low priority option in SQLite
+ */
+ function lowPriorityOption() {
+ return '';
+ }
+
+ /**
+ * Returns an SQL expression for a simple conditional.
+ * - uses CASE on SQLite
+ */
+ function conditional($cond, $trueVal, $falseVal) {
+ return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
+ }
+
+ function wasDeadlock() {
+ return $this->lastErrno() == SQLITE_BUSY;
+ }
+
+ /**
+ * @return string wikitext of a link to the server software's web site
+ */
+ function getSoftwareLink() {
+ return "[http://sqlite.org/ SQLite]";
+ }
+
+ /**
+ * @return string Version information from the database
+ */
+ function getServerVersion() {
+ global $wgContLang;
+ $ver = $this->mConn->getAttribute(PDO::ATTR_SERVER_VERSION);
+ $size = $wgContLang->formatSize(filesize($this->mDatabaseFile));
+ $file = basename($this->mDatabaseFile);
+ return $ver." ($file: $size)";
+ }
+
+ /**
+ * Query whether a given column exists in the mediawiki schema
+ */
+ function fieldExists($table, $field) { return true; }
+
+ function fieldInfo($table, $field) { return SQLiteField::fromText($this, $table, $field); }
+
+ function begin() {
+ if ($this->mTrxLevel == 1) $this->commit();
+ $this->mConn->beginTransaction();
+ $this->mTrxLevel = 1;
+ }
+
+ function commit() {
+ if ($this->mTrxLevel == 0) return;
+ $this->mConn->commit();
+ $this->mTrxLevel = 0;
+ }
+
+ function rollback() {
+ if ($this->mTrxLevel == 0) return;
+ $this->mConn->rollBack();
+ $this->mTrxLevel = 0;
+ }
+
+ function limitResultForUpdate($sql, $num) {
+ return $sql;
+ }
+
+ function strencode($s) {
+ return substr($this->addQuotes($s),1,-1);
+ }
+
+ function encodeBlob($b) {
+ return $this->strencode($b);
+ }
+
+ function decodeBlob($b) {
+ return $b;
+ }
+
+ function addQuotes($s) {
+ return $this->mConn->quote($s);
+ }
+
+ function quote_ident($s) { return $s; }
+
+ /**
+ * For now, does nothing
+ */
+ function selectDB($db) { return true; }
+
+ /**
+ * not done
+ */
+ public function setTimeout($timeout) { return; }
+
+ function ping() {
+ wfDebug("Function ping() not written for SQLite yet");
+ return true;
+ }
+
+ /**
+ * How lagged is this slave?
+ */
+ public function getLag() {
+ return 0;
+ }
+
+ /**
+ * Called by the installer script (when modified according to the MediaWikiLite installation instructions)
+ * - this is the same way PostgreSQL works, MySQL reads in tables.sql and interwiki.sql using dbsource (which calls db->sourceFile)
+ */
+ public function setup_database() {
+ global $IP,$wgSQLiteDataDir,$wgDBTableOptions;
+ $wgDBTableOptions = '';
+ $mysql_tmpl = "$IP/maintenance/tables.sql";
+ $mysql_iw = "$IP/maintenance/interwiki.sql";
+ $sqlite_tmpl = "$IP/maintenance/sqlite/tables.sql";
+
+ # Make an SQLite template file if it doesn't exist (based on the same one MySQL uses to create a new wiki db)
+ if (!file_exists($sqlite_tmpl)) {
+ $sql = file_get_contents($mysql_tmpl);
+ $sql = preg_replace('/^\s*--.*?$/m','',$sql); # strip comments
+ $sql = preg_replace('/^\s*(UNIQUE)?\s*(PRIMARY)?\s*KEY.+?$/m','',$sql);
+ $sql = preg_replace('/^\s*(UNIQUE )?INDEX.+?$/m','',$sql); # These indexes should be created with a CREATE INDEX query
+ $sql = preg_replace('/^\s*FULLTEXT.+?$/m','',$sql); # Full text indexes
+ $sql = preg_replace('/ENUM\(.+?\)/','TEXT',$sql); # Make ENUM's into TEXT's
+ $sql = preg_replace('/binary\(\d+\)/','BLOB',$sql);
+ $sql = preg_replace('/(TYPE|MAX_ROWS|AVG_ROW_LENGTH)=\w+/','',$sql);
+ $sql = preg_replace('/,\s*\)/s',')',$sql); # removing previous items may leave a trailing comma
+ $sql = str_replace('binary','',$sql);
+ $sql = str_replace('auto_increment','PRIMARY KEY AUTOINCREMENT',$sql);
+ $sql = str_replace(' unsigned','',$sql);
+ $sql = str_replace(' int ',' INTEGER ',$sql);
+ $sql = str_replace('NOT NULL','',$sql);
+
+ # Tidy up and write file
+ $sql = preg_replace('/^\s*^/m','',$sql); # Remove empty lines
+ $sql = preg_replace('/;$/m',";\n",$sql); # Separate each statement with an empty line
+ file_put_contents($sqlite_tmpl,$sql);
+ }
+
+ # Parse the SQLite template replacing inline variables such as /*$wgDBprefix*/
+ $err = $this->sourceFile($sqlite_tmpl);
+ if ($err !== true) $this->reportQueryError($err,0,$sql,__FUNCTION__);
+
+ # Use DatabasePostgres's code to populate interwiki from MySQL template
+ $f = fopen($mysql_iw,'r');
+ if ($f == false) dieout("<li>Could not find the interwiki.sql file");
+ $sql = "INSERT INTO interwiki(iw_prefix,iw_url,iw_local) VALUES ";
+ while (!feof($f)) {
+ $line = fgets($f,1024);
+ $matches = array();
+ if (!preg_match('/^\s*(\(.+?),(\d)\)/', $line, $matches)) continue;
+ $this->query("$sql $matches[1],$matches[2])");
+ }
+ }
+
+ /**
+ * No-op lock functions
+ */
+ public function lock( $lockName, $method ) {
+ return true;
+ }
+ public function unlock( $lockName, $method ) {
+ return true;
+ }
+
+}
+
+/**
+ * @ingroup Database
+ */
+class SQLiteField extends MySQLField {
+
+ function __construct() {
+ }
+
+ static function fromText($db, $table, $field) {
+ $n = new SQLiteField;
+ $n->name = $field;
+ $n->tablename = $table;
+ return $n;
+ }
+
+} // end DatabaseSqlite class
+
diff --git a/includes/db/LBFactory.php b/includes/db/LBFactory.php
new file mode 100644
index 00000000..256875d7
--- /dev/null
+++ b/includes/db/LBFactory.php
@@ -0,0 +1,261 @@
+<?php
+/**
+ * @file
+ * @ingroup Database
+ */
+
+/**
+ * An interface for generating database load balancers
+ * @ingroup Database
+ */
+abstract class LBFactory {
+ static $instance;
+
+ /**
+ * Get an LBFactory instance
+ */
+ static function &singleton() {
+ if ( is_null( self::$instance ) ) {
+ global $wgLBFactoryConf;
+ $class = $wgLBFactoryConf['class'];
+ self::$instance = new $class( $wgLBFactoryConf );
+ }
+ return self::$instance;
+ }
+
+ /**
+ * Shut down, close connections and destroy the cached instance.
+ *
+ */
+ static function destroyInstance() {
+ if ( self::$instance ) {
+ self::$instance->shutdown();
+ self::$instance->forEachLBCallMethod( 'closeAll' );
+ self::$instance = null;
+ }
+ }
+
+ /**
+ * Construct a factory based on a configuration array (typically from $wgLBFactoryConf)
+ */
+ abstract function __construct( $conf );
+
+ /**
+ * Create a new load balancer object. The resulting object will be untracked,
+ * not chronology-protected, and the caller is responsible for cleaning it up.
+ *
+ * @param string $wiki Wiki ID, or false for the current wiki
+ * @return LoadBalancer
+ */
+ abstract function newMainLB( $wiki = false );
+
+ /**
+ * Get a cached (tracked) load balancer object.
+ *
+ * @param string $wiki Wiki ID, or false for the current wiki
+ * @return LoadBalancer
+ */
+ abstract function getMainLB( $wiki = false );
+
+ /*
+ * Create a new load balancer for external storage. The resulting object will be
+ * untracked, not chronology-protected, and the caller is responsible for
+ * cleaning it up.
+ *
+ * @param string $cluster External storage cluster, or false for core
+ * @param string $wiki Wiki ID, or false for the current wiki
+ */
+ abstract function newExternalLB( $cluster, $wiki = false );
+
+ /*
+ * Get a cached (tracked) load balancer for external storage
+ *
+ * @param string $cluster External storage cluster, or false for core
+ * @param string $wiki Wiki ID, or false for the current wiki
+ */
+ abstract function &getExternalLB( $cluster, $wiki = false );
+
+ /**
+ * Execute a function for each tracked load balancer
+ * The callback is called with the load balancer as the first parameter,
+ * and $params passed as the subsequent parameters.
+ */
+ abstract function forEachLB( $callback, $params = array() );
+
+ /**
+ * Prepare all tracked load balancers for shutdown
+ * STUB
+ */
+ function shutdown() {}
+
+ /**
+ * Call a method of each tracked load balancer
+ */
+ function forEachLBCallMethod( $methodName, $args = array() ) {
+ $this->forEachLB( array( $this, 'callMethod' ), array( $methodName, $args ) );
+ }
+
+ /**
+ * Private helper for forEachLBCallMethod
+ */
+ function callMethod( $loadBalancer, $methodName, $args ) {
+ call_user_func_array( array( $loadBalancer, $methodName ), $args );
+ }
+
+ /**
+ * Commit changes on all master connections
+ */
+ function commitMasterChanges() {
+ $this->forEachLBCallMethod( 'commitMasterChanges' );
+ }
+}
+
+/**
+ * A simple single-master LBFactory that gets its configuration from the b/c globals
+ */
+class LBFactory_Simple extends LBFactory {
+ var $mainLB;
+ var $extLBs = array();
+
+ # Chronology protector
+ var $chronProt;
+
+ function __construct( $conf ) {
+ $this->chronProt = new ChronologyProtector;
+ }
+
+ function newMainLB( $wiki = false ) {
+ global $wgDBservers, $wgMasterWaitTimeout;
+ if ( $wgDBservers ) {
+ $servers = $wgDBservers;
+ } else {
+ global $wgDBserver, $wgDBuser, $wgDBpassword, $wgDBname, $wgDBtype, $wgDebugDumpSql;
+ $servers = array(array(
+ 'host' => $wgDBserver,
+ 'user' => $wgDBuser,
+ 'password' => $wgDBpassword,
+ 'dbname' => $wgDBname,
+ 'type' => $wgDBtype,
+ 'load' => 1,
+ 'flags' => ($wgDebugDumpSql ? DBO_DEBUG : 0) | DBO_DEFAULT
+ ));
+ }
+
+ return new LoadBalancer( array(
+ 'servers' => $servers,
+ 'masterWaitTimeout' => $wgMasterWaitTimeout
+ ));
+ }
+
+ function getMainLB( $wiki = false ) {
+ if ( !isset( $this->mainLB ) ) {
+ $this->mainLB = $this->newMainLB( $wiki );
+ $this->mainLB->parentInfo( array( 'id' => 'main' ) );
+ $this->chronProt->initLB( $this->mainLB );
+ }
+ return $this->mainLB;
+ }
+
+ function newExternalLB( $cluster, $wiki = false ) {
+ global $wgExternalServers;
+ if ( !isset( $wgExternalServers[$cluster] ) ) {
+ throw new MWException( __METHOD__.": Unknown cluster \"$cluster\"" );
+ }
+ return new LoadBalancer( array(
+ 'servers' => $wgExternalServers[$cluster]
+ ));
+ }
+
+ function &getExternalLB( $cluster, $wiki = false ) {
+ if ( !isset( $this->extLBs[$cluster] ) ) {
+ $this->extLBs[$cluster] = $this->newExternalLB( $cluster, $wiki );
+ $this->extLBs[$cluster]->parentInfo( array( 'id' => "ext-$cluster" ) );
+ }
+ return $this->extLBs[$cluster];
+ }
+
+ /**
+ * Execute a function for each tracked load balancer
+ * The callback is called with the load balancer as the first parameter,
+ * and $params passed as the subsequent parameters.
+ */
+ function forEachLB( $callback, $params = array() ) {
+ if ( isset( $this->mainLB ) ) {
+ call_user_func_array( $callback, array_merge( array( $this->mainLB ), $params ) );
+ }
+ foreach ( $this->extLBs as $lb ) {
+ call_user_func_array( $callback, array_merge( array( $lb ), $params ) );
+ }
+ }
+
+ function shutdown() {
+ if ( $this->mainLB ) {
+ $this->chronProt->shutdownLB( $this->mainLB );
+ }
+ $this->chronProt->shutdown();
+ $this->commitMasterChanges();
+ }
+}
+
+/**
+ * Class for ensuring a consistent ordering of events as seen by the user, despite replication.
+ * Kind of like Hawking's [[Chronology Protection Agency]].
+ */
+class ChronologyProtector {
+ var $startupPos;
+ var $shutdownPos = array();
+
+ /**
+ * Initialise a LoadBalancer to give it appropriate chronology protection.
+ *
+ * @param LoadBalancer $lb
+ */
+ function initLB( $lb ) {
+ if ( $this->startupPos === null ) {
+ if ( !empty( $_SESSION[__CLASS__] ) ) {
+ $this->startupPos = $_SESSION[__CLASS__];
+ }
+ }
+ if ( !$this->startupPos ) {
+ return;
+ }
+ $masterName = $lb->getServerName( 0 );
+
+ if ( $lb->getServerCount() > 1 && !empty( $this->startupPos[$masterName] ) ) {
+ $info = $lb->parentInfo();
+ $pos = $this->startupPos[$masterName];
+ wfDebug( __METHOD__.": LB " . $info['id'] . " waiting for master pos $pos\n" );
+ $lb->waitFor( $this->startupPos[$masterName] );
+ }
+ }
+
+ /**
+ * Notify the ChronologyProtector that the LoadBalancer is about to shut
+ * down. Saves replication positions.
+ *
+ * @param LoadBalancer $lb
+ */
+ function shutdownLB( $lb ) {
+ if ( session_id() != '' && $lb->getServerCount() > 1 ) {
+ $masterName = $lb->getServerName( 0 );
+ if ( !isset( $this->shutdownPos[$masterName] ) ) {
+ $pos = $lb->getMasterPos();
+ $info = $lb->parentInfo();
+ wfDebug( __METHOD__.": LB " . $info['id'] . " has master pos $pos\n" );
+ $this->shutdownPos[$masterName] = $pos;
+ }
+ }
+ }
+
+ /**
+ * Notify the ChronologyProtector that the LBFactory is done calling shutdownLB() for now.
+ * May commit chronology data to persistent storage.
+ */
+ function shutdown() {
+ if ( session_id() != '' && count( $this->shutdownPos ) ) {
+ wfDebug( __METHOD__.": saving master pos for " .
+ count( $this->shutdownPos ) . " master(s)\n" );
+ $_SESSION[__CLASS__] = $this->shutdownPos;
+ }
+ }
+}
diff --git a/includes/db/LBFactory_Multi.php b/includes/db/LBFactory_Multi.php
new file mode 100644
index 00000000..48c2d99b
--- /dev/null
+++ b/includes/db/LBFactory_Multi.php
@@ -0,0 +1,233 @@
+<?php
+/**
+ * @file
+ * @ingroup Database
+ */
+
+
+/**
+ * A multi-wiki, multi-master factory for Wikimedia and similar installations.
+ * Ignores the old configuration globals
+ *
+ * Configuration:
+ * sectionsByDB A map of database names to section names
+ *
+ * sectionLoads A 2-d map. For each section, gives a map of server names to load ratios.
+ * For example: array( 'section1' => array( 'db1' => 100, 'db2' => 100 ) )
+ *
+ * serverTemplate A server info associative array as documented for $wgDBservers. The host,
+ * hostName and load entries will be overridden.
+ *
+ * groupLoadsBySection A 3-d map giving server load ratios for each section and group. For example:
+ * array( 'section1' => array( 'group1' => array( 'db1' => 100, 'db2' => 100 ) ) )
+ *
+ * groupLoadsByDB A 3-d map giving server load ratios by DB name.
+ *
+ * hostsByName A map of hostname to IP address.
+ *
+ * externalLoads A map of external storage cluster name to server load map
+ *
+ * externalTemplateOverrides A set of server info keys overriding serverTemplate for external storage
+ *
+ * templateOverridesByServer A 2-d map overriding serverTemplate and externalTemplateOverrides on a
+ * server-by-server basis. Applies to both core and external storage.
+ *
+ * templateOverridesByCluster A 2-d map overriding the server info by external storage cluster
+ *
+ * masterTemplateOverrides An override array for all master servers.
+ *
+ * @ingroup Database
+ */
+class LBFactory_Multi extends LBFactory {
+ // Required settings
+ var $sectionsByDB, $sectionLoads, $serverTemplate;
+ // Optional settings
+ var $groupLoadsBySection = array(), $groupLoadsByDB = array(), $hostsByName = array();
+ var $externalLoads = array(), $externalTemplateOverrides, $templateOverridesByServer;
+ var $templateOverridesByCluster, $masterTemplateOverrides;
+ // Other stuff
+ var $conf, $mainLBs = array(), $extLBs = array();
+ var $lastWiki, $lastSection;
+
+ function __construct( $conf ) {
+ $this->chronProt = new ChronologyProtector;
+ $this->conf = $conf;
+ $required = array( 'sectionsByDB', 'sectionLoads', 'serverTemplate' );
+ $optional = array( 'groupLoadsBySection', 'groupLoadsByDB', 'hostsByName',
+ 'externalLoads', 'externalTemplateOverrides', 'templateOverridesByServer',
+ 'templateOverridesByCluster', 'masterTemplateOverrides' );
+
+ foreach ( $required as $key ) {
+ if ( !isset( $conf[$key] ) ) {
+ throw new MWException( __CLASS__.": $key is required in configuration" );
+ }
+ $this->$key = $conf[$key];
+ }
+
+ foreach ( $optional as $key ) {
+ if ( isset( $conf[$key] ) ) {
+ $this->$key = $conf[$key];
+ }
+ }
+ }
+
+ function getSectionForWiki( $wiki = false ) {
+ if ( $this->lastWiki === $wiki ) {
+ return $this->lastSection;
+ }
+ list( $dbName, $prefix ) = $this->getDBNameAndPrefix( $wiki );
+ if ( isset( $this->sectionsByDB[$dbName] ) ) {
+ $section = $this->sectionsByDB[$dbName];
+ } else {
+ $section = 'DEFAULT';
+ }
+ $this->lastSection = $section;
+ $this->lastWiki = $wiki;
+ return $section;
+ }
+
+ function newMainLB( $wiki = false ) {
+ list( $dbName, $prefix ) = $this->getDBNameAndPrefix( $wiki );
+ $section = $this->getSectionForWiki( $wiki );
+ $groupLoads = array();
+ if ( isset( $this->groupLoadsByDB[$dbName] ) ) {
+ $groupLoads = $this->groupLoadsByDB[$dbName];
+ }
+ if ( isset( $this->groupLoadsBySection[$section] ) ) {
+ $groupLoads = array_merge_recursive( $groupLoads, $this->groupLoadsBySection[$section] );
+ }
+ return $this->newLoadBalancer( $this->serverTemplate, $this->sectionLoads[$section], $groupLoads );
+ }
+
+ function getMainLB( $wiki = false ) {
+ $section = $this->getSectionForWiki( $wiki );
+ if ( !isset( $this->mainLBs[$section] ) ) {
+ $lb = $this->newMainLB( $wiki, $section );
+ $this->chronProt->initLB( $lb );
+ $lb->parentInfo( array( 'id' => "main-$section" ) );
+ $this->mainLBs[$section] = $lb;
+ }
+ return $this->mainLBs[$section];
+ }
+
+ function newExternalLB( $cluster, $wiki = false ) {
+ if ( !isset( $this->externalLoads[$cluster] ) ) {
+ throw new MWException( __METHOD__.": Unknown cluster \"$cluster\"" );
+ }
+ $template = $this->serverTemplate;
+ if ( isset( $this->externalTemplateOverrides ) ) {
+ $template = $this->externalTemplateOverrides + $template;
+ }
+ if ( isset( $this->templateOverridesByCluster[$cluster] ) ) {
+ $template = $this->templateOverridesByCluster[$cluster] + $template;
+ }
+ return $this->newLoadBalancer( $template, $this->externalLoads[$cluster], array() );
+ }
+
+ function &getExternalLB( $cluster, $wiki = false ) {
+ if ( !isset( $this->extLBs[$cluster] ) ) {
+ $this->extLBs[$cluster] = $this->newExternalLB( $cluster, $wiki );
+ $this->extLBs[$cluster]->parentInfo( array( 'id' => "ext-$cluster" ) );
+ }
+ return $this->extLBs[$cluster];
+ }
+
+ /**
+ * Make a new load balancer object based on template and load array
+ */
+ function newLoadBalancer( $template, $loads, $groupLoads ) {
+ global $wgMasterWaitTimeout;
+ $servers = $this->makeServerArray( $template, $loads, $groupLoads );
+ $lb = new LoadBalancer( array(
+ 'servers' => $servers,
+ 'masterWaitTimeout' => $wgMasterWaitTimeout
+ ));
+ return $lb;
+ }
+
+ /**
+ * Make a server array as expected by LoadBalancer::__construct, using a template and load array
+ */
+ function makeServerArray( $template, $loads, $groupLoads ) {
+ $servers = array();
+ $master = true;
+ $groupLoadsByServer = $this->reindexGroupLoads( $groupLoads );
+ foreach ( $groupLoadsByServer as $server => $stuff ) {
+ if ( !isset( $loads[$server] ) ) {
+ $loads[$server] = 0;
+ }
+ }
+ foreach ( $loads as $serverName => $load ) {
+ $serverInfo = $template;
+ if ( $master ) {
+ $serverInfo['master'] = true;
+ if ( isset( $this->masterTemplateOverrides ) ) {
+ $serverInfo = $this->masterTemplateOverrides + $serverInfo;
+ }
+ $master = false;
+ }
+ if ( isset( $this->templateOverridesByServer[$serverName] ) ) {
+ $serverInfo = $this->templateOverridesByServer[$serverName] + $serverInfo;
+ }
+ if ( isset( $groupLoadsByServer[$serverName] ) ) {
+ $serverInfo['groupLoads'] = $groupLoadsByServer[$serverName];
+ }
+ if ( isset( $this->hostsByName[$serverName] ) ) {
+ $serverInfo['host'] = $this->hostsByName[$serverName];
+ } else {
+ $serverInfo['host'] = $serverName;
+ }
+ $serverInfo['hostName'] = $serverName;
+ $serverInfo['load'] = $load;
+ $servers[] = $serverInfo;
+ }
+ return $servers;
+ }
+
+ /**
+ * Take a group load array indexed by group then server, and reindex it by server then group
+ */
+ function reindexGroupLoads( $groupLoads ) {
+ $reindexed = array();
+ foreach ( $groupLoads as $group => $loads ) {
+ foreach ( $loads as $server => $load ) {
+ $reindexed[$server][$group] = $load;
+ }
+ }
+ return $reindexed;
+ }
+
+ /**
+ * Get the database name and prefix based on the wiki ID
+ */
+ function getDBNameAndPrefix( $wiki = false ) {
+ if ( $wiki === false ) {
+ global $wgDBname, $wgDBprefix;
+ return array( $wgDBname, $wgDBprefix );
+ } else {
+ return wfSplitWikiID( $wiki );
+ }
+ }
+
+ /**
+ * Execute a function for each tracked load balancer
+ * The callback is called with the load balancer as the first parameter,
+ * and $params passed as the subsequent parameters.
+ */
+ function forEachLB( $callback, $params = array() ) {
+ foreach ( $this->mainLBs as $lb ) {
+ call_user_func_array( $callback, array_merge( array( $lb ), $params ) );
+ }
+ foreach ( $this->extLBs as $lb ) {
+ call_user_func_array( $callback, array_merge( array( $lb ), $params ) );
+ }
+ }
+
+ function shutdown() {
+ foreach ( $this->mainLBs as $lb ) {
+ $this->chronProt->shutdownLB( $lb );
+ }
+ $this->chronProt->shutdown();
+ $this->commitMasterChanges();
+ }
+}
diff --git a/includes/db/LoadBalancer.php b/includes/db/LoadBalancer.php
new file mode 100644
index 00000000..42c4044d
--- /dev/null
+++ b/includes/db/LoadBalancer.php
@@ -0,0 +1,918 @@
+<?php
+/**
+ * @file
+ * @ingroup Database
+ */
+
+/**
+ * Database load balancing object
+ *
+ * @todo document
+ * @ingroup Database
+ */
+class LoadBalancer {
+ /* private */ var $mServers, $mConns, $mLoads, $mGroupLoads;
+ /* private */ var $mFailFunction, $mErrorConnection;
+ /* private */ var $mReadIndex, $mLastIndex, $mAllowLagged;
+ /* private */ var $mWaitForPos, $mWaitTimeout;
+ /* private */ var $mLaggedSlaveMode, $mLastError = 'Unknown error';
+ /* private */ var $mParentInfo, $mLagTimes;
+ /* private */ var $mLoadMonitorClass, $mLoadMonitor;
+
+ /**
+ * @param array $params Array with keys:
+ * servers Required. Array of server info structures.
+ * failFunction Deprecated, use exceptions instead.
+ * masterWaitTimeout Replication lag wait timeout
+ * loadMonitor Name of a class used to fetch server lag and load.
+ */
+ function __construct( $params )
+ {
+ if ( !isset( $params['servers'] ) ) {
+ throw new MWException( __CLASS__.': missing servers parameter' );
+ }
+ $this->mServers = $params['servers'];
+
+ if ( isset( $params['failFunction'] ) ) {
+ $this->mFailFunction = $params['failFunction'];
+ } else {
+ $this->mFailFunction = false;
+ }
+ if ( isset( $params['waitTimeout'] ) ) {
+ $this->mWaitTimeout = $params['waitTimeout'];
+ } else {
+ $this->mWaitTimeout = 10;
+ }
+
+ $this->mReadIndex = -1;
+ $this->mWriteIndex = -1;
+ $this->mConns = array(
+ 'local' => array(),
+ 'foreignUsed' => array(),
+ 'foreignFree' => array() );
+ $this->mLastIndex = -1;
+ $this->mLoads = array();
+ $this->mWaitForPos = false;
+ $this->mLaggedSlaveMode = false;
+ $this->mErrorConnection = false;
+ $this->mAllowLag = false;
+ $this->mLoadMonitorClass = isset( $params['loadMonitor'] )
+ ? $params['loadMonitor'] : 'LoadMonitor_MySQL';
+
+ foreach( $params['servers'] as $i => $server ) {
+ $this->mLoads[$i] = $server['load'];
+ if ( isset( $server['groupLoads'] ) ) {
+ foreach ( $server['groupLoads'] as $group => $ratio ) {
+ if ( !isset( $this->mGroupLoads[$group] ) ) {
+ $this->mGroupLoads[$group] = array();
+ }
+ $this->mGroupLoads[$group][$i] = $ratio;
+ }
+ }
+ }
+ }
+
+ static function newFromParams( $servers, $failFunction = false, $waitTimeout = 10 )
+ {
+ return new LoadBalancer( $servers, $failFunction, $waitTimeout );
+ }
+
+ /**
+ * Get a LoadMonitor instance
+ */
+ function getLoadMonitor() {
+ if ( !isset( $this->mLoadMonitor ) ) {
+ $class = $this->mLoadMonitorClass;
+ $this->mLoadMonitor = new $class( $this );
+ }
+ return $this->mLoadMonitor;
+ }
+
+ /**
+ * Get or set arbitrary data used by the parent object, usually an LBFactory
+ */
+ function parentInfo( $x = null ) {
+ return wfSetVar( $this->mParentInfo, $x );
+ }
+
+ /**
+ * Given an array of non-normalised probabilities, this function will select
+ * an element and return the appropriate key
+ */
+ function pickRandom( $weights )
+ {
+ if ( !is_array( $weights ) || count( $weights ) == 0 ) {
+ return false;
+ }
+
+ $sum = array_sum( $weights );
+ if ( $sum == 0 ) {
+ # No loads on any of them
+ # In previous versions, this triggered an unweighted random selection,
+ # but this feature has been removed as of April 2006 to allow for strict
+ # separation of query groups.
+ return false;
+ }
+ $max = mt_getrandmax();
+ $rand = mt_rand(0, $max) / $max * $sum;
+
+ $sum = 0;
+ foreach ( $weights as $i => $w ) {
+ $sum += $w;
+ if ( $sum >= $rand ) {
+ break;
+ }
+ }
+ return $i;
+ }
+
+ function getRandomNonLagged( $loads, $wiki = false ) {
+ # Unset excessively lagged servers
+ $lags = $this->getLagTimes( $wiki );
+ foreach ( $lags as $i => $lag ) {
+ if ( $i != 0 && isset( $this->mServers[$i]['max lag'] ) ) {
+ if ( $lag === false ) {
+ wfDebug( "Server #$i is not replicating\n" );
+ unset( $loads[$i] );
+ } elseif ( $lag > $this->mServers[$i]['max lag'] ) {
+ wfDebug( "Server #$i is excessively lagged ($lag seconds)\n" );
+ unset( $loads[$i] );
+ }
+ }
+ }
+
+ # Find out if all the slaves with non-zero load are lagged
+ $sum = 0;
+ foreach ( $loads as $load ) {
+ $sum += $load;
+ }
+ if ( $sum == 0 ) {
+ # No appropriate DB servers except maybe the master and some slaves with zero load
+ # Do NOT use the master
+ # Instead, this function will return false, triggering read-only mode,
+ # and a lagged slave will be used instead.
+ return false;
+ }
+
+ if ( count( $loads ) == 0 ) {
+ return false;
+ }
+
+ #wfDebugLog( 'connect', var_export( $loads, true ) );
+
+ # Return a random representative of the remainder
+ return $this->pickRandom( $loads );
+ }
+
+ /**
+ * Get the index of the reader connection, which may be a slave
+ * This takes into account load ratios and lag times. It should
+ * always return a consistent index during a given invocation
+ *
+ * Side effect: opens connections to databases
+ */
+ function getReaderIndex( $group = false, $wiki = false ) {
+ global $wgReadOnly, $wgDBClusterTimeout, $wgDBAvgStatusPoll, $wgDBtype;
+
+ # FIXME: For now, only go through all this for mysql databases
+ if ($wgDBtype != 'mysql') {
+ return $this->getWriterIndex();
+ }
+
+ if ( count( $this->mServers ) == 1 ) {
+ # Skip the load balancing if there's only one server
+ return 0;
+ } elseif ( $group === false and $this->mReadIndex >= 0 ) {
+ # Shortcut if generic reader exists already
+ return $this->mReadIndex;
+ }
+
+ wfProfileIn( __METHOD__ );
+
+ $totalElapsed = 0;
+
+ # convert from seconds to microseconds
+ $timeout = $wgDBClusterTimeout * 1e6;
+
+ # Find the relevant load array
+ if ( $group !== false ) {
+ if ( isset( $this->mGroupLoads[$group] ) ) {
+ $nonErrorLoads = $this->mGroupLoads[$group];
+ } else {
+ # No loads for this group, return false and the caller can use some other group
+ wfDebug( __METHOD__.": no loads for group $group\n" );
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+ } else {
+ $nonErrorLoads = $this->mLoads;
+ }
+
+ if ( !$nonErrorLoads ) {
+ throw new MWException( "Empty server array given to LoadBalancer" );
+ }
+
+ # Scale the configured load ratios according to the dynamic load (if the load monitor supports it)
+ $this->getLoadMonitor()->scaleLoads( $nonErrorLoads, $group, $wiki );
+
+ $i = false;
+ $found = false;
+ $laggedSlaveMode = false;
+
+ # First try quickly looking through the available servers for a server that
+ # meets our criteria
+ do {
+ $totalThreadsConnected = 0;
+ $overloadedServers = 0;
+ $currentLoads = $nonErrorLoads;
+ while ( count( $currentLoads ) ) {
+ if ( $wgReadOnly || $this->mAllowLagged || $laggedSlaveMode ) {
+ $i = $this->pickRandom( $currentLoads );
+ } else {
+ $i = $this->getRandomNonLagged( $currentLoads, $wiki );
+ if ( $i === false && count( $currentLoads ) != 0 ) {
+ # All slaves lagged. Switch to read-only mode
+ $wgReadOnly = wfMsgNoDBForContent( 'readonly_lag' );
+ $i = $this->pickRandom( $currentLoads );
+ $laggedSlaveMode = true;
+ }
+ }
+
+ if ( $i === false ) {
+ # pickRandom() returned false
+ # This is permanent and means the configuration or the load monitor
+ # wants us to return false.
+ wfDebugLog( 'connect', __METHOD__.": pickRandom() returned false\n" );
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+
+ wfDebugLog( 'connect', __METHOD__.": Using reader #$i: {$this->mServers[$i]['host']}...\n" );
+ $conn = $this->openConnection( $i, $wiki );
+
+ if ( !$conn ) {
+ wfDebugLog( 'connect', __METHOD__.": Failed connecting to $i/$wiki\n" );
+ unset( $nonErrorLoads[$i] );
+ unset( $currentLoads[$i] );
+ continue;
+ }
+
+ // Perform post-connection backoff
+ $threshold = isset( $this->mServers[$i]['max threads'] )
+ ? $this->mServers[$i]['max threads'] : false;
+ $backoff = $this->getLoadMonitor()->postConnectionBackoff( $conn, $threshold );
+
+ // Decrement reference counter, we are finished with this connection.
+ // It will be incremented for the caller later.
+ if ( $wiki !== false ) {
+ $this->reuseConnection( $conn );
+ }
+
+ if ( $backoff ) {
+ # Post-connection overload, don't use this server for now
+ $totalThreadsConnected += $backoff;
+ $overloadedServers++;
+ unset( $currentLoads[$i] );
+ } else {
+ # Return this server
+ break 2;
+ }
+ }
+
+ # No server found yet
+ $i = false;
+
+ # If all servers were down, quit now
+ if ( !count( $nonErrorLoads ) ) {
+ wfDebugLog( 'connect', "All servers down\n" );
+ break;
+ }
+
+ # Some servers must have been overloaded
+ if ( $overloadedServers == 0 ) {
+ throw new MWException( __METHOD__.": unexpectedly found no overloaded servers" );
+ }
+ # Back off for a while
+ # Scale the sleep time by the number of connected threads, to produce a
+ # roughly constant global poll rate
+ $avgThreads = $totalThreadsConnected / $overloadedServers;
+ $totalElapsed += $this->sleep( $wgDBAvgStatusPoll * $avgThreads );
+ } while ( $totalElapsed < $timeout );
+
+ if ( $totalElapsed >= $timeout ) {
+ wfDebugLog( 'connect', "All servers busy\n" );
+ $this->mErrorConnection = false;
+ $this->mLastError = 'All servers busy';
+ }
+
+ if ( $i !== false ) {
+ # Slave connection successful
+ # Wait for the session master pos for a short time
+ if ( $this->mWaitForPos && $i > 0 ) {
+ if ( !$this->doWait( $i ) ) {
+ $this->mServers[$i]['slave pos'] = $conn->getSlavePos();
+ }
+ }
+ if ( $this->mReadIndex <=0 && $this->mLoads[$i]>0 && $i !== false ) {
+ $this->mReadIndex = $i;
+ }
+ }
+ wfProfileOut( __METHOD__ );
+ return $i;
+ }
+
+ /**
+ * Wait for a specified number of microseconds, and return the period waited
+ */
+ function sleep( $t ) {
+ wfProfileIn( __METHOD__ );
+ wfDebug( __METHOD__.": waiting $t us\n" );
+ usleep( $t );
+ wfProfileOut( __METHOD__ );
+ return $t;
+ }
+
+ /**
+ * Get a random server to use in a query group
+ * @deprecated use getReaderIndex
+ */
+ function getGroupIndex( $group ) {
+ return $this->getReaderIndex( $group );
+ }
+
+ /**
+ * Set the master wait position
+ * If a DB_SLAVE connection has been opened already, waits
+ * Otherwise sets a variable telling it to wait if such a connection is opened
+ */
+ public function waitFor( $pos ) {
+ wfProfileIn( __METHOD__ );
+ $this->mWaitForPos = $pos;
+ $i = $this->mReadIndex;
+
+ if ( $i > 0 ) {
+ if ( !$this->doWait( $i ) ) {
+ $this->mServers[$i]['slave pos'] = $this->getAnyOpenConnection( $i )->getSlavePos();
+ $this->mLaggedSlaveMode = true;
+ }
+ }
+ wfProfileOut( __METHOD__ );
+ }
+
+ /**
+ * Get any open connection to a given server index, local or foreign
+ * Returns false if there is no connection open
+ */
+ function getAnyOpenConnection( $i ) {
+ foreach ( $this->mConns as $type => $conns ) {
+ if ( !empty( $conns[$i] ) ) {
+ return reset( $conns[$i] );
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Wait for a given slave to catch up to the master pos stored in $this
+ */
+ function doWait( $index ) {
+ # Find a connection to wait on
+ $conn = $this->getAnyOpenConnection( $index );
+ if ( !$conn ) {
+ wfDebug( __METHOD__ . ": no connection open\n" );
+ return false;
+ }
+
+ wfDebug( __METHOD__.": Waiting for slave #$index to catch up...\n" );
+ $result = $conn->masterPosWait( $this->mWaitForPos, $this->mWaitTimeout );
+
+ if ( $result == -1 || is_null( $result ) ) {
+ # Timed out waiting for slave, use master instead
+ wfDebug( __METHOD__.": Timed out waiting for slave #$index pos {$this->mWaitForPos}\n" );
+ return false;
+ } else {
+ wfDebug( __METHOD__.": Done\n" );
+ return true;
+ }
+ }
+
+ /**
+ * Get a connection by index
+ * This is the main entry point for this class.
+ */
+ public function &getConnection( $i, $groups = array(), $wiki = false ) {
+ global $wgDBtype;
+ wfProfileIn( __METHOD__ );
+
+ if ( $wiki === wfWikiID() ) {
+ $wiki = false;
+ }
+
+ # Query groups
+ if ( $i == DB_MASTER ) {
+ $i = $this->getWriterIndex();
+ } elseif ( !is_array( $groups ) ) {
+ $groupIndex = $this->getReaderIndex( $groups, $wiki );
+ if ( $groupIndex !== false ) {
+ $serverName = $this->getServerName( $groupIndex );
+ wfDebug( __METHOD__.": using server $serverName for group $groups\n" );
+ $i = $groupIndex;
+ }
+ } else {
+ foreach ( $groups as $group ) {
+ $groupIndex = $this->getReaderIndex( $group, $wiki );
+ if ( $groupIndex !== false ) {
+ $serverName = $this->getServerName( $groupIndex );
+ wfDebug( __METHOD__.": using server $serverName for group $group\n" );
+ $i = $groupIndex;
+ break;
+ }
+ }
+ }
+
+ # 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->reportConnectionError( $this->mErrorConnection );
+ }
+
+ # Now we have an explicit index into the servers array
+ $conn = $this->openConnection( $i, $wiki );
+ if ( !$conn ) {
+ $this->reportConnectionError( $this->mErrorConnection );
+ }
+
+ wfProfileOut( __METHOD__ );
+ return $conn;
+ }
+
+ /**
+ * Mark a foreign connection as being available for reuse under a different
+ * DB name or prefix. This mechanism is reference-counted, and must be called
+ * the same number of times as getConnection() to work.
+ */
+ public function reuseConnection( $conn ) {
+ $serverIndex = $conn->getLBInfo('serverIndex');
+ $refCount = $conn->getLBInfo('foreignPoolRefCount');
+ $dbName = $conn->getDBname();
+ $prefix = $conn->tablePrefix();
+ if ( strval( $prefix ) !== '' ) {
+ $wiki = "$dbName-$prefix";
+ } else {
+ $wiki = $dbName;
+ }
+ if ( $serverIndex === null || $refCount === null ) {
+ wfDebug( __METHOD__.": this connection was not opened as a foreign connection\n" );
+ /**
+ * This can happen in code like:
+ * foreach ( $dbs as $db ) {
+ * $conn = $lb->getConnection( DB_SLAVE, array(), $db );
+ * ...
+ * $lb->reuseConnection( $conn );
+ * }
+ * When a connection to the local DB is opened in this way, reuseConnection()
+ * should be ignored
+ */
+ return;
+ }
+ if ( $this->mConns['foreignUsed'][$serverIndex][$wiki] !== $conn ) {
+ throw new MWException( __METHOD__.": connection not found, has the connection been freed already?" );
+ }
+ $conn->setLBInfo( 'foreignPoolRefCount', --$refCount );
+ if ( $refCount <= 0 ) {
+ $this->mConns['foreignFree'][$serverIndex][$wiki] = $conn;
+ unset( $this->mConns['foreignUsed'][$serverIndex][$wiki] );
+ wfDebug( __METHOD__.": freed connection $serverIndex/$wiki\n" );
+ } else {
+ wfDebug( __METHOD__.": reference count for $serverIndex/$wiki reduced to $refCount\n" );
+ }
+ }
+
+ /**
+ * Open a connection to the server given by the specified index
+ * Index must be an actual index into the array.
+ * If the server is already open, returns it.
+ *
+ * On error, returns false, and the connection which caused the
+ * error will be available via $this->mErrorConnection.
+ *
+ * @param integer $i Server index
+ * @param string $wiki Wiki ID to open
+ * @return Database
+ *
+ * @access private
+ */
+ function openConnection( $i, $wiki = false ) {
+ wfProfileIn( __METHOD__ );
+ if ( $wiki !== false ) {
+ $conn = $this->openForeignConnection( $i, $wiki );
+ wfProfileOut( __METHOD__);
+ return $conn;
+ }
+ if ( isset( $this->mConns['local'][$i][0] ) ) {
+ $conn = $this->mConns['local'][$i][0];
+ } else {
+ $server = $this->mServers[$i];
+ $server['serverIndex'] = $i;
+ $conn = $this->reallyOpenConnection( $server );
+ if ( $conn->isOpen() ) {
+ $this->mConns['local'][$i][0] = $conn;
+ } else {
+ wfDebug( "Failed to connect to database $i at {$this->mServers[$i]['host']}\n" );
+ $this->mErrorConnection = $conn;
+ $conn = false;
+ }
+ }
+ $this->mLastIndex = $i;
+ wfProfileOut( __METHOD__ );
+ return $conn;
+ }
+
+ /**
+ * Open a connection to a foreign DB, or return one if it is already open.
+ *
+ * Increments a reference count on the returned connection which locks the
+ * connection to the requested wiki. This reference count can be
+ * decremented by calling reuseConnection().
+ *
+ * If a connection is open to the appropriate server already, but with the wrong
+ * database, it will be switched to the right database and returned, as long as
+ * it has been freed first with reuseConnection().
+ *
+ * On error, returns false, and the connection which caused the
+ * error will be available via $this->mErrorConnection.
+ *
+ * @param integer $i Server index
+ * @param string $wiki Wiki ID to open
+ * @return Database
+ */
+ function openForeignConnection( $i, $wiki ) {
+ wfProfileIn(__METHOD__);
+ list( $dbName, $prefix ) = wfSplitWikiID( $wiki );
+ if ( isset( $this->mConns['foreignUsed'][$i][$wiki] ) ) {
+ // Reuse an already-used connection
+ $conn = $this->mConns['foreignUsed'][$i][$wiki];
+ wfDebug( __METHOD__.": reusing connection $i/$wiki\n" );
+ } elseif ( isset( $this->mConns['foreignFree'][$i][$wiki] ) ) {
+ // Reuse a free connection for the same wiki
+ $conn = $this->mConns['foreignFree'][$i][$wiki];
+ unset( $this->mConns['foreignFree'][$i][$wiki] );
+ $this->mConns['foreignUsed'][$i][$wiki] = $conn;
+ wfDebug( __METHOD__.": reusing free connection $i/$wiki\n" );
+ } elseif ( !empty( $this->mConns['foreignFree'][$i] ) ) {
+ // Reuse a connection from another wiki
+ $conn = reset( $this->mConns['foreignFree'][$i] );
+ $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";
+ $this->mErrorConnection = $conn;
+ $conn = false;
+ } else {
+ $conn->tablePrefix( $prefix );
+ unset( $this->mConns['foreignFree'][$i][$oldWiki] );
+ $this->mConns['foreignUsed'][$i][$wiki] = $conn;
+ wfDebug( __METHOD__.": reusing free connection from $oldWiki for $wiki\n" );
+ }
+ } else {
+ // Open a new connection
+ $server = $this->mServers[$i];
+ $server['serverIndex'] = $i;
+ $server['foreignPoolRefCount'] = 0;
+ $conn = $this->reallyOpenConnection( $server, $dbName );
+ if ( !$conn->isOpen() ) {
+ wfDebug( __METHOD__.": error opening connection for $i/$wiki\n" );
+ $this->mErrorConnection = $conn;
+ $conn = false;
+ } else {
+ $this->mConns['foreignUsed'][$i][$wiki] = $conn;
+ wfDebug( __METHOD__.": opened new connection for $i/$wiki\n" );
+ }
+ }
+
+ // Increment reference count
+ if ( $conn ) {
+ $refCount = $conn->getLBInfo( 'foreignPoolRefCount' );
+ $conn->setLBInfo( 'foreignPoolRefCount', $refCount + 1 );
+ }
+ wfProfileOut(__METHOD__);
+ return $conn;
+ }
+
+ /**
+ * Test if the specified index represents an open connection
+ * @access private
+ */
+ function isOpen( $index ) {
+ if( !is_integer( $index ) ) {
+ return false;
+ }
+ return (bool)$this->getAnyOpenConnection( $index );
+ }
+
+ /**
+ * Really opens a connection. Uncached.
+ * Returns a Database object whether or not the connection was successful.
+ * @access private
+ */
+ function reallyOpenConnection( $server, $dbNameOverride = false ) {
+ if( !is_array( $server ) ) {
+ throw new MWException( 'You must update your load-balancing configuration. See DefaultSettings.php entry for $wgDBservers.' );
+ }
+
+ extract( $server );
+ if ( $dbNameOverride !== false ) {
+ $dbname = $dbNameOverride;
+ }
+
+ # Get class for this database type
+ $class = 'Database' . ucfirst( $type );
+
+ # Create object
+ wfDebug( "Connecting to $host $dbname...\n" );
+ $db = new $class( $host, $user, $password, $dbname, 1, $flags );
+ if ( $db->isOpen() ) {
+ wfDebug( "Connected\n" );
+ } else {
+ wfDebug( "Failed\n" );
+ }
+ $db->setLBInfo( $server );
+ if ( isset( $server['fakeSlaveLag'] ) ) {
+ $db->setFakeSlaveLag( $server['fakeSlaveLag'] );
+ }
+ if ( isset( $server['fakeMaster'] ) ) {
+ $db->setFakeMaster( true );
+ }
+ return $db;
+ }
+
+ 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 );
+ }
+ } else {
+ if ( $this->mFailFunction ) {
+ $conn->failFunction( $this->mFailFunction );
+ } else {
+ $conn->failFunction( false );
+ }
+ $server = $conn->getProperty( 'mServer' );
+ $conn->reportConnectionError( "{$this->mLastError} ({$server})" );
+ }
+ $reporting = false;
+ }
+ wfProfileOut( __METHOD__ );
+ }
+
+ function getWriterIndex() {
+ return 0;
+ }
+
+ /**
+ * Returns true if the specified index is a valid server index
+ */
+ function haveIndex( $i ) {
+ return array_key_exists( $i, $this->mServers );
+ }
+
+ /**
+ * Returns true if the specified index is valid and has non-zero load
+ */
+ function isNonZeroLoad( $i ) {
+ return array_key_exists( $i, $this->mServers ) && $this->mLoads[$i] != 0;
+ }
+
+ /**
+ * Get the number of defined servers (not the number of open connections)
+ */
+ function getServerCount() {
+ return count( $this->mServers );
+ }
+
+ /**
+ * Get the host name or IP address of the server with the specified index
+ * Prefer a readable name if available.
+ */
+ function getServerName( $i ) {
+ if ( isset( $this->mServers[$i]['hostName'] ) ) {
+ return $this->mServers[$i]['hostName'];
+ } elseif ( isset( $this->mServers[$i]['host'] ) ) {
+ return $this->mServers[$i]['host'];
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Return the server info structure for a given index, or false if the index is invalid.
+ */
+ function getServerInfo( $i ) {
+ if ( isset( $this->mServers[$i] ) ) {
+ return $this->mServers[$i];
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Get the current master position for chronology control purposes
+ * @return mixed
+ */
+ function getMasterPos() {
+ # If this entire request was served from a slave without opening a connection to the
+ # master (however unlikely that may be), then we can fetch the position from the slave.
+ $masterConn = $this->getAnyOpenConnection( 0 );
+ if ( !$masterConn ) {
+ for ( $i = 1; $i < count( $this->mServers ); $i++ ) {
+ $conn = $this->getAnyOpenConnection( $i );
+ if ( $conn ) {
+ wfDebug( "Master pos fetched from slave\n" );
+ return $conn->getSlavePos();
+ }
+ }
+ } else {
+ wfDebug( "Master pos fetched from master\n" );
+ return $masterConn->getMasterPos();
+ }
+ return false;
+ }
+
+ /**
+ * Close all open connections
+ */
+ function closeAll() {
+ foreach ( $this->mConns as $conns2 ) {
+ foreach ( $conns2 as $conns3 ) {
+ foreach ( $conns3 as $conn ) {
+ $conn->close();
+ }
+ }
+ }
+ $this->mConns = array(
+ 'local' => array(),
+ 'foreignFree' => array(),
+ 'foreignUsed' => array(),
+ );
+ }
+
+ /**
+ * Close a connection
+ * Using this function makes sure the LoadBalancer knows the connection is closed.
+ * If you use $conn->close() directly, the load balancer won't update its state.
+ */
+ function closeConnecton( $conn ) {
+ $done = false;
+ foreach ( $this->mConns as $i1 => $conns2 ) {
+ foreach ( $conns2 as $i2 => $conns3 ) {
+ foreach ( $conns3 as $i3 => $candidateConn ) {
+ if ( $conn === $candidateConn ) {
+ $conn->close();
+ unset( $this->mConns[$i1][$i2][$i3] );
+ $done = true;
+ break;
+ }
+ }
+ }
+ }
+ if ( !$done ) {
+ $conn->close();
+ }
+ }
+
+ /**
+ * Commit transactions on all open connections
+ */
+ function commitAll() {
+ foreach ( $this->mConns as $conns2 ) {
+ foreach ( $conns2 as $conns3 ) {
+ foreach ( $conns3 as $conn ) {
+ $conn->immediateCommit();
+ }
+ }
+ }
+ }
+
+ /* Issue COMMIT only on master, only if queries were done on connection */
+ function commitMasterChanges() {
+ // Always 0, but who knows.. :)
+ $masterIndex = $this->getWriterIndex();
+ foreach ( $this->mConns as $type => $conns2 ) {
+ if ( empty( $conns2[$masterIndex] ) ) {
+ continue;
+ }
+ foreach ( $conns2[$masterIndex] as $conn ) {
+ if ( $conn->lastQuery() != '' ) {
+ $conn->commit();
+ }
+ }
+ }
+ }
+
+ function waitTimeout( $value = NULL ) {
+ return wfSetVar( $this->mWaitTimeout, $value );
+ }
+
+ function getLaggedSlaveMode() {
+ return $this->mLaggedSlaveMode;
+ }
+
+ /* Disables/enables lag checks */
+ function allowLagged($mode=null) {
+ if ($mode===null)
+ return $this->mAllowLagged;
+ $this->mAllowLagged=$mode;
+ }
+
+ function pingAll() {
+ $success = true;
+ foreach ( $this->mConns as $conns2 ) {
+ foreach ( $conns2 as $conns3 ) {
+ foreach ( $conns3 as $conn ) {
+ if ( !$conn->ping() ) {
+ $success = false;
+ }
+ }
+ }
+ }
+ return $success;
+ }
+
+ /**
+ * Call a function with each open connection object
+ */
+ function forEachOpenConnection( $callback, $params = array() ) {
+ foreach ( $this->mConns as $conns2 ) {
+ foreach ( $conns2 as $conns3 ) {
+ foreach ( $conns3 as $conn ) {
+ $mergedParams = array_merge( array( $conn ), $params );
+ call_user_func_array( $callback, $mergedParams );
+ }
+ }
+ }
+ }
+
+ /**
+ * Get the hostname and lag time of the most-lagged slave.
+ * This is useful for maintenance scripts that need to throttle their updates.
+ * May attempt to open connections to slaves on the default DB.
+ */
+ function getMaxLag() {
+ $maxLag = -1;
+ $host = '';
+ foreach ( $this->mServers as $i => $conn ) {
+ $conn = $this->getAnyOpenConnection( $i );
+ if ( !$conn ) {
+ $conn = $this->openConnection( $i );
+ }
+ if ( !$conn ) {
+ continue;
+ }
+ $lag = $conn->getLag();
+ if ( $lag > $maxLag ) {
+ $maxLag = $lag;
+ $host = $this->mServers[$i]['host'];
+ }
+ }
+ return array( $host, $maxLag );
+ }
+
+ /**
+ * Get lag time for each server
+ * Results are cached for a short time in memcached, and indefinitely in the process cache
+ */
+ function getLagTimes( $wiki = false ) {
+ # Try process cache
+ if ( isset( $this->mLagTimes ) ) {
+ return $this->mLagTimes;
+ }
+ # No, send the request to the load monitor
+ $this->mLagTimes = $this->getLoadMonitor()->getLagTimes( array_keys( $this->mServers ), $wiki );
+ return $this->mLagTimes;
+ }
+}
diff --git a/includes/db/LoadMonitor.php b/includes/db/LoadMonitor.php
new file mode 100644
index 00000000..8e16f1a1
--- /dev/null
+++ b/includes/db/LoadMonitor.php
@@ -0,0 +1,121 @@
+<?php
+
+/**
+ * An interface for database load monitoring
+ */
+
+interface LoadMonitor {
+ /**
+ * Construct a new LoadMonitor with a given LoadBalancer parent
+ */
+ function __construct( $parent );
+
+ /**
+ * Perform pre-connection load ratio adjustment.
+ * @param array $loads
+ * @param string $group The selected query group
+ * @param string $wiki
+ */
+ function scaleLoads( &$loads, $group = false, $wiki = false );
+
+ /**
+ * Perform post-connection backoff.
+ *
+ * If the connection is in overload, this should return a backoff factor
+ * which will be used to control polling time. The number of threads
+ * connected is a good measure.
+ *
+ * If there is no overload, zero can be returned.
+ *
+ * A threshold thread count is given, the concrete class may compare this
+ * to the running thread count. The threshold may be false, which indicates
+ * that the sysadmin has not configured this feature.
+ *
+ * @param Database $conn
+ * @param float $threshold
+ */
+ function postConnectionBackoff( $conn, $threshold );
+
+ /**
+ * Return an estimate of replication lag for each server
+ */
+ function getLagTimes( $serverIndexes, $wiki );
+}
+
+
+/**
+ * Basic MySQL load monitor with no external dependencies
+ * Uses memcached to cache the replication lag for a short time
+ */
+
+class LoadMonitor_MySQL implements LoadMonitor {
+ var $parent; // LoadBalancer
+
+ function __construct( $parent ) {
+ $this->parent = $parent;
+ }
+
+ function scaleLoads( &$loads, $group = false, $wiki = false ) {
+ }
+
+ function getLagTimes( $serverIndexes, $wiki ) {
+ wfProfileIn( __METHOD__ );
+ $expiry = 5;
+ $requestRate = 10;
+
+ global $wgMemc;
+ $masterName = $this->parent->getServerName( 0 );
+ $memcKey = wfMemcKey( 'lag_times', $masterName );
+ $times = $wgMemc->get( $memcKey );
+ if ( $times ) {
+ # Randomly recache with probability rising over $expiry
+ $elapsed = time() - $times['timestamp'];
+ $chance = max( 0, ( $expiry - $elapsed ) * $requestRate );
+ if ( mt_rand( 0, $chance ) != 0 ) {
+ unset( $times['timestamp'] );
+ wfProfileOut( __METHOD__ );
+ return $times;
+ }
+ wfIncrStats( 'lag_cache_miss_expired' );
+ } else {
+ wfIncrStats( 'lag_cache_miss_absent' );
+ }
+
+ # Cache key missing or expired
+
+ $times = array();
+ foreach ( $serverIndexes as $i ) {
+ if ($i == 0) { # Master
+ $times[$i] = 0;
+ } elseif ( false !== ( $conn = $this->parent->getAnyOpenConnection( $i ) ) ) {
+ $times[$i] = $conn->getLag();
+ } elseif ( false !== ( $conn = $this->parent->openConnection( $i, $wiki ) ) ) {
+ $times[$i] = $conn->getLag();
+ }
+ }
+
+ # Add a timestamp key so we know when it was cached
+ $times['timestamp'] = time();
+ $wgMemc->set( $memcKey, $times, $expiry );
+
+ # But don't give the timestamp to the caller
+ unset($times['timestamp']);
+ $lagTimes = $times;
+
+ wfProfileOut( __METHOD__ );
+ return $lagTimes;
+ }
+
+ function postConnectionBackoff( $conn, $threshold ) {
+ if ( !$threshold ) {
+ return 0;
+ }
+ $status = $conn->getStatus("Thread%");
+ if ( $status['Threads_running'] > $threshold ) {
+ return $status['Threads_connected'];
+ } else {
+ return 0;
+ }
+ }
+}
+
diff --git a/includes/filerepo/ArchivedFile.php b/includes/filerepo/ArchivedFile.php
index cc70b26d..646256bb 100644
--- a/includes/filerepo/ArchivedFile.php
+++ b/includes/filerepo/ArchivedFile.php
@@ -1,7 +1,7 @@
<?php
/**
- * @addtogroup Media
+ * @ingroup Media
*/
class ArchivedFile
{
@@ -26,9 +26,9 @@ class ArchivedFile
$timestamp, # time of upload
$dataLoaded, # Whether or not all this has been loaded from the database (loadFromXxx)
$deleted; # Bitfield akin to rev_deleted
-
- /**#@-*/
-
+
+ /**#@-*/
+
function ArchivedFile( $title, $id=0, $key='' ) {
if( !is_object($title) ) {
throw new MWException( 'ArchivedFile constructor given bogus title.' );
@@ -84,19 +84,19 @@ class ArchivedFile
'fa_user_text',
'fa_timestamp',
'fa_deleted' ),
- array(
+ array(
'fa_name' => $this->title->getDBkey(),
$conds ),
__METHOD__,
array( 'ORDER BY' => 'fa_timestamp DESC' ) );
-
+
if ( $dbr->numRows( $res ) == 0 ) {
// this revision does not exist?
return;
}
$ret = $dbr->resultObject( $res );
$row = $ret->fetchObject();
-
+
// initialize fields for filestore image object
$this->id = intval($row->fa_id);
$this->name = $row->fa_name;
@@ -120,17 +120,17 @@ class ArchivedFile
return;
}
$this->dataLoaded = true;
-
+
return true;
}
/**
* Loads a file object from the filearchive table
* @return ResultWrapper
- */
- public static function newFromRow( $row ) {
+ */
+ public static function newFromRow( $row ) {
$file = new ArchivedFile( Title::makeTitle( NS_IMAGE, $row->fa_name ) );
-
+
$file->id = intval($row->fa_id);
$file->name = $row->fa_name;
$file->archive_name = $row->fa_archive_name;
@@ -148,41 +148,41 @@ class ArchivedFile
$file->user_text = $row->fa_user_text;
$file->timestamp = $row->fa_timestamp;
$file->deleted = $row->fa_deleted;
-
+
return $file;
}
-
+
/**
* Return the associated title object
* @public
*/
- public function getTitle() {
+ public function getTitle() {
return $this->title;
}
/**
* Return the file name
- */
- public function getName() {
+ */
+ public function getName() {
return $this->name;
}
- public function getID() {
+ public function getID() {
$this->load();
return $this->id;
}
-
+
/**
* Return the FileStore key
- */
- public function getKey() {
+ */
+ public function getKey() {
$this->load();
return $this->key;
}
-
+
/**
* Return the FileStore storage group
- */
+ */
public function getGroup() {
return $file->group;
}
@@ -192,7 +192,7 @@ class ArchivedFile
*/
public function getWidth() {
$this->load();
- return $this->width;
+ return $this->width;
}
/**
@@ -200,9 +200,9 @@ class ArchivedFile
*/
public function getHeight() {
$this->load();
- return $this->height;
+ return $this->height;
}
-
+
/**
* Get handler-specific metadata
*/
@@ -219,7 +219,7 @@ class ArchivedFile
$this->load();
return $this->size;
}
-
+
/**
* Return the bits of the image file, in bytes
* @public
@@ -228,7 +228,7 @@ class ArchivedFile
$this->load();
return $this->bits;
}
-
+
/**
* Returns the mime type of the file.
*/
@@ -236,7 +236,7 @@ class ArchivedFile
$this->load();
return $this->mime;
}
-
+
/**
* Return the type of the media in the file.
* Use the value returned by this function with the MEDIATYPE_xxx constants.
@@ -265,7 +265,7 @@ class ArchivedFile
return $this->user;
}
}
-
+
/**
* Return the user name of the uploader.
*/
@@ -277,7 +277,7 @@ class ArchivedFile
return $this->user_text;
}
}
-
+
/**
* Return upload description.
*/
@@ -289,7 +289,7 @@ class ArchivedFile
return $this->description;
}
}
-
+
/**
* Return the user ID of the uploader.
*/
@@ -297,7 +297,7 @@ class ArchivedFile
$this->load();
return $this->user;
}
-
+
/**
* Return the user name of the uploader.
*/
@@ -305,7 +305,7 @@ class ArchivedFile
$this->load();
return $this->user_text;
}
-
+
/**
* Return upload description.
*/
@@ -322,18 +322,18 @@ class ArchivedFile
public function isDeleted( $field ) {
return ($this->deleted & $field) == $field;
}
-
+
/**
* Determine if the current user is allowed to view a particular
* field of this FileStore image file, if it's marked as deleted.
- * @param int $field
+ * @param int $field
* @return bool
*/
public function userCan( $field ) {
if( ($this->deleted & $field) == $field ) {
global $wgUser;
$permission = ( $this->deleted & File::DELETED_RESTRICTED ) == File::DELETED_RESTRICTED
- ? 'hiderevision'
+ ? 'suppressrevision'
: 'deleterevision';
wfDebug( "Checking for $permission due to $field match on $this->deleted\n" );
return $wgUser->isAllowed( $permission );
diff --git a/includes/filerepo/FSRepo.php b/includes/filerepo/FSRepo.php
index 86887d09..08ec1514 100644
--- a/includes/filerepo/FSRepo.php
+++ b/includes/filerepo/FSRepo.php
@@ -3,8 +3,8 @@
/**
* A repository for files accessible via the local filesystem. Does not support
* database access or registration.
+ * @ingroup FileRepo
*/
-
class FSRepo extends FileRepo {
var $directory, $deletedDir, $url, $hashLevels, $deletedHashLevels;
var $fileFactory = array( 'UnregisteredLocalFile', 'newFromTitle' );
@@ -20,7 +20,7 @@ class FSRepo extends FileRepo {
// Optional settings
$this->hashLevels = isset( $info['hashLevels'] ) ? $info['hashLevels'] : 2;
- $this->deletedHashLevels = isset( $info['deletedHashLevels'] ) ?
+ $this->deletedHashLevels = isset( $info['deletedHashLevels'] ) ?
$info['deletedHashLevels'] : $this->hashLevels;
$this->deletedDir = isset( $info['deletedDir'] ) ? $info['deletedDir'] : false;
}
@@ -80,7 +80,7 @@ class FSRepo extends FileRepo {
/**
* Get a URL referring to this repository, with the private mwrepo protocol.
- * The suffix, if supplied, is considered to be unencoded, and will be
+ * The suffix, if supplied, is considered to be unencoded, and will be
* URL-encoded before being returned.
*/
function getVirtualUrl( $suffix = false ) {
@@ -121,10 +121,13 @@ class FSRepo extends FileRepo {
* @param integer $flags Bitwise combination of the following flags:
* self::DELETE_SOURCE Delete the source file after upload
* self::OVERWRITE Overwrite an existing destination file instead of failing
- * self::OVERWRITE_SAME Overwrite the file if the destination exists and has the
+ * self::OVERWRITE_SAME Overwrite the file if the destination exists and has the
* same contents as the source
*/
function storeBatch( $triplets, $flags = 0 ) {
+ if ( !wfMkdirParents( $this->directory ) ) {
+ return $this->newFatal( 'upload_directory_missing', $this->directory );
+ }
if ( !is_writable( $this->directory ) ) {
return $this->newFatal( 'upload_directory_read_only', $this->directory );
}
@@ -146,13 +149,13 @@ class FSRepo extends FileRepo {
if ( !wfMkdirParents( $dstDir ) ) {
return $this->newFatal( 'directorycreateerror', $dstDir );
}
- // In the deleted zone, seed new directories with a blank
+ // In the deleted zone, seed new directories with a blank
// index.html, to prevent crawling
if ( $dstZone == 'deleted' ) {
file_put_contents( "$dstDir/index.html", '' );
}
}
-
+
if ( self::isVirtualUrl( $srcPath ) ) {
$srcPath = $triplets[$i][0] = $this->resolveVirtualUrl( $srcPath );
}
@@ -213,7 +216,7 @@ class FSRepo extends FileRepo {
/**
* Pick a random name in the temp zone and store a file to it.
- * @param string $originalName The base name of the file as specified
+ * @param string $originalName The base name of the file as specified
* by the user. The file extension will be maintained.
* @param string $srcPath The current location of the file.
* @return FileRepoStatus object with the URL in the value.
@@ -255,6 +258,9 @@ class FSRepo extends FileRepo {
*/
function publishBatch( $triplets, $flags = 0 ) {
// Perform initial checks
+ if ( !wfMkdirParents( $this->directory ) ) {
+ return $this->newFatal( 'upload_directory_missing', $this->directory );
+ }
if ( !is_writable( $this->directory ) ) {
return $this->newFatal( 'upload_directory_read_only', $this->directory );
}
@@ -273,7 +279,7 @@ class FSRepo extends FileRepo {
}
$dstPath = "{$this->directory}/$dstRel";
$archivePath = "{$this->directory}/$archiveRel";
-
+
$dstDir = dirname( $dstPath );
$archiveDir = dirname( $archivePath );
// Abort immediately on directory creation errors since they're likely to be repetitive
@@ -292,7 +298,7 @@ class FSRepo extends FileRepo {
if ( !$status->ok ) {
return $status;
}
-
+
foreach ( $triplets as $i => $triplet ) {
list( $srcPath, $dstRel, $archiveRel ) = $triplet;
$dstPath = "{$this->directory}/$dstRel";
@@ -302,8 +308,8 @@ class FSRepo extends FileRepo {
if( is_file( $dstPath ) ) {
// Check if the archive file exists
// This is a sanity check to avoid data loss. In UNIX, the rename primitive
- // unlinks the destination file if it exists. DB-based synchronisation in
- // publishBatch's caller should prevent races. In Windows there's no
+ // unlinks the destination file if it exists. DB-based synchronisation in
+ // publishBatch's caller should prevent races. In Windows there's no
// problem because the rename primitive fails if the destination exists.
if ( is_file( $archivePath ) ) {
$success = false;
@@ -354,12 +360,12 @@ class FSRepo extends FileRepo {
/**
* Move a group of files to the deletion archive.
- * If no valid deletion archive is configured, this may either delete the
+ * If no valid deletion archive is configured, this may either delete the
* file or throw an exception, depending on the preference of the repository.
*
- * @param array $sourceDestPairs Array of source/destination pairs. Each element
+ * @param array $sourceDestPairs Array of source/destination pairs. Each element
* is a two-element array containing the source file path relative to the
- * public root in the first element, and the archive file path relative
+ * public root in the first element, and the archive file path relative
* to the deleted zone root in the second element.
* @return FileRepoStatus
*/
@@ -403,7 +409,7 @@ class FSRepo extends FileRepo {
/**
* Move the files
- * We're now committed to returning an OK result, which will lead to
+ * We're now committed to returning an OK result, which will lead to
* the files being moved in the DB also.
*/
foreach ( $sourceDestPairs as $pair ) {
@@ -433,7 +439,7 @@ class FSRepo extends FileRepo {
}
return $status;
}
-
+
/**
* Get a relative path including trailing slash, e.g. f/fa/
* If the repo is not hashed, returns an empty string
@@ -443,7 +449,7 @@ class FSRepo extends FileRepo {
}
/**
- * Get a relative path for a deletion archive key,
+ * Get a relative path for a deletion archive key,
* e.g. s/z/a/ for sza251lrxrc1jad41h5mgilp8nysje52.jpg
*/
function getDeletedHashPath( $key ) {
@@ -453,7 +459,7 @@ class FSRepo extends FileRepo {
}
return $path;
}
-
+
/**
* Call a callback function for every file in the repository.
* Uses the filesystem even in child classes.
@@ -526,5 +532,3 @@ class FSRepo extends FileRepo {
}
}
-
-
diff --git a/includes/filerepo/File.php b/includes/filerepo/File.php
index 5172ad0f..64b48e0a 100644
--- a/includes/filerepo/File.php
+++ b/includes/filerepo/File.php
@@ -1,15 +1,15 @@
<?php
/**
- * Implements some public methods and some protected utility functions which
- * are required by multiple child classes. Contains stub functionality for
+ * Implements some public methods and some protected utility functions which
+ * are required by multiple child classes. Contains stub functionality for
* unimplemented public methods.
*
- * Stub functions which should be overridden are marked with STUB. Some more
+ * Stub functions which should be overridden are marked with STUB. Some more
* concrete functions are also typically overridden by child classes.
*
* Note that only the repo object knows what its file class is called. You should
- * never name a file class explictly outside of the repo class. Instead use the
+ * never name a file class explictly outside of the repo class. Instead use the
* repo's factory functions to generate file objects, for example:
*
* RepoGroup::singleton()->getLocalRepo()->newFile($title);
@@ -17,7 +17,7 @@
* The convenience functions wfLocalFile() and wfFindFile() should be sufficient
* in most cases.
*
- * @addtogroup FileRepo
+ * @ingroup FileRepo
*/
abstract class File {
const DELETED_FILE = 1;
@@ -28,17 +28,17 @@ abstract class File {
const DELETE_SOURCE = 1;
- /**
- * Some member variables can be lazy-initialised using __get(). The
+ /**
+ * Some member variables can be lazy-initialised using __get(). The
* initialisation function for these variables is always a function named
- * like getVar(), where Var is the variable name with upper-case first
+ * like getVar(), where Var is the variable name with upper-case first
* letter.
*
* The following variables are initialised in this way in this base class:
- * name, extension, handler, path, canRender, isSafeFile,
+ * name, extension, handler, path, canRender, isSafeFile,
* transformScript, hashPath, pageCount, url
*
- * Code within this class should generally use the accessor function
+ * Code within this class should generally use the accessor function
* directly, since __get() isn't re-entrant and therefore causes bugs that
* depend on initialisation order.
*/
@@ -46,7 +46,7 @@ abstract class File {
/**
* The following member variables are not lazy-initialised
*/
- var $repo, $title, $lastError, $redirected;
+ var $repo, $title, $lastError, $redirected, $redirectedTitle;
/**
* Call this constructor from child classes
@@ -79,7 +79,8 @@ abstract class File {
'htm' => 'html',
'jpeg' => 'jpg',
'mpeg' => 'mpg',
- 'tiff' => 'tif' );
+ 'tiff' => 'tif',
+ 'ogv' => 'ogg' );
if( isset( $squish[$lower] ) ) {
return $squish[$lower];
} elseif( preg_match( '/^[0-9a-z]+$/', $lower ) ) {
@@ -90,6 +91,21 @@ abstract class File {
}
/**
+ * Checks if file extensions are compatible
+ *
+ * @param $old File Old file
+ * @param $new string New name
+ */
+ static function checkExtensionCompatibility( File $old, $new ) {
+ $oldMime = $old->getMimeType();
+ $n = strrpos( $new, '.' );
+ $newExt = self::normalizeExtension(
+ $n ? substr( $new, $n + 1 ) : '' );
+ $mimeMagic = MimeMagic::singleton();
+ return $mimeMagic->isMatchingExtension( $newExt, $oldMime );
+ }
+
+ /**
* Upgrade the database row if there is one
* Called by ImagePage
* STUB
@@ -110,7 +126,7 @@ abstract class File {
return array( $mime, 'unknown' );
}
}
-
+
/**
* Return the name of this file
*/
@@ -118,7 +134,7 @@ abstract class File {
if ( !isset( $this->name ) ) {
$this->name = $this->repo->getNameFromTitle( $this->title );
}
- return $this->name;
+ return $this->name;
}
/**
@@ -127,7 +143,7 @@ abstract class File {
function getExtension() {
if ( !isset( $this->extension ) ) {
$n = strrpos( $this->getName(), '.' );
- $this->extension = self::normalizeExtension(
+ $this->extension = self::normalizeExtension(
$n ? substr( $this->getName(), $n + 1 ) : '' );
}
return $this->extension;
@@ -137,17 +153,26 @@ abstract class File {
* Return the associated title object
*/
public function getTitle() { return $this->title; }
+
+ /**
+ * Return the title used to find this file
+ */
+ public function getOriginalTitle() {
+ if ( $this->redirected )
+ return $this->getRedirectedTitle();
+ return $this->title;
+ }
/**
* Return the URL of the file
*/
- public function getUrl() {
+ public function getUrl() {
if ( !isset( $this->url ) ) {
$this->url = $this->repo->getZoneUrl( 'public' ) . '/' . $this->getUrlRel();
}
- return $this->url;
+ return $this->url;
}
-
+
/**
* Return a fully-qualified URL to the file.
* Upload URL paths _may or may not_ be fully qualified, so
@@ -197,7 +222,7 @@ abstract class File {
}
/**
- * Return the width of the image. Returns false if the width is unknown
+ * Return the width of the image. Returns false if the width is unknown
* or undefined.
*
* STUB
@@ -206,7 +231,7 @@ abstract class File {
public function getWidth( $page = 1 ) { return false; }
/**
- * Return the height of the image. Returns false if the height is unknown
+ * Return the height of the image. Returns false if the height is unknown
* or undefined
*
* STUB
@@ -264,8 +289,8 @@ abstract class File {
function getMediaType() { return MEDIATYPE_UNKNOWN; }
/**
- * Checks if the output of transform() for this file is likely
- * to be valid. If this is false, various user elements will
+ * Checks if the output of transform() for this file is likely
+ * to be valid. If this is false, various user elements will
* display a placeholder instead.
*
* Currently, this checks if the file is an image format
@@ -325,7 +350,7 @@ abstract class File {
}
return $this->isSafeFile;
}
-
+
/** Accessor for __get() */
protected function getIsSafeFile() {
return $this->isSafeFile();
@@ -371,13 +396,24 @@ abstract class File {
* Returns true if file exists in the repository.
*
* Overridden by LocalFile to avoid unnecessary stat calls.
- *
+ *
* @return boolean Whether file exists in the repository.
*/
public function exists() {
return $this->getPath() && file_exists( $this->path );
}
+ /**
+ * Returns true if file exists in the repository and can be included in a page.
+ * It would be unsafe to include private images, making public thumbnails inadvertently
+ *
+ * @return boolean Whether file exists in the repository and is includable.
+ * @public
+ */
+ function isVisible() {
+ return $this->exists();
+ }
+
function getTransformScript() {
if ( !isset( $this->transformScript ) ) {
$this->transformScript = false;
@@ -482,7 +518,7 @@ abstract class File {
/**
* Transform a media file
*
- * @param array $params An associative array of handler-specific parameters. Typical
+ * @param array $params An associative array of handler-specific parameters. Typical
* keys are width, height and page.
* @param integer $flags A bitfield, may contain self::RENDER_NOW to force rendering
* @return MediaTransformOutput
@@ -509,10 +545,10 @@ abstract class File {
$normalisedParams = $params;
$this->handler->normaliseParams( $this, $normalisedParams );
- $thumbName = $this->thumbName( $normalisedParams );
+ $thumbName = $this->thumbName( $normalisedParams );
$thumbPath = $this->getThumbPath( $thumbName );
$thumbUrl = $this->getThumbUrl( $thumbName );
-
+
if ( $this->repo->canTransformVia404() && !($flags & self::RENDER_NOW ) ) {
$thumb = $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
break;
@@ -536,8 +572,11 @@ abstract class File {
}
}
- if ( $wgUseSquid ) {
- wfPurgeSquidServers( array( $thumbUrl ) );
+ // 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()) ) {
+ SquidUpdate::purge( array( $thumbUrl ) );
}
} while (false);
@@ -545,7 +584,7 @@ abstract class File {
return $thumb;
}
- /**
+ /**
* Hook into transform() to allow migration of thumbnail files
* STUB
* Overridden by LocalFile
@@ -555,7 +594,7 @@ abstract class File {
/**
* Get a MediaHandler instance for this file
*/
- function getHandler() {
+ function getHandler() {
if ( !isset( $this->handler ) ) {
$this->handler = MediaHandler::getHandler( $this->getMimeType() );
}
@@ -614,7 +653,7 @@ abstract class File {
$title->purgeSquid();
}
}
-
+
/**
* Purge metadata and all affected pages when the file is created,
* deleted, or majorly updated.
@@ -641,12 +680,12 @@ abstract class File {
* @param $end timestamp Only revisions newer than $end will be returned
*/
function getHistory($limit = null, $start = null, $end = null) {
- return false;
+ return array();
}
/**
- * Return the history of this file, line by line. Starts with current version,
- * then old versions. Should return an object similar to an image/oldimage
+ * Return the history of this file, line by line. Starts with current version,
+ * then old versions. Should return an object similar to an image/oldimage
* database row.
*
* STUB
@@ -712,7 +751,7 @@ abstract class File {
/** Get the path of the archive directory, or a particular file if $suffix is specified */
function getArchivePath( $suffix = false ) {
- return $this->repo->getZonePath('public') . '/' . $this->getArchiveRel();
+ return $this->repo->getZonePath('public') . '/' . $this->getArchiveRel( $suffix );
}
/** Get the path of the thumbnail directory, or a particular file if $suffix is specified */
@@ -767,7 +806,7 @@ abstract class File {
$path .= '/' . rawurlencode( $suffix );
}
return $path;
- }
+ }
/**
* @return bool
@@ -785,25 +824,25 @@ abstract class File {
* STUB
* Overridden by LocalFile
*/
- function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '', $watch = false ) {
- $this->readOnlyError();
+ function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '', $watch = false ) {
+ $this->readOnlyError();
}
/**
- * Move or copy a file to its public location. If a file exists at the
- * destination, move it to an archive. Returns the archive name on success
- * or an empty string if it was a new file, and a wikitext-formatted
- * WikiError object on failure.
+ * Move or copy a file to its public location. If a file exists at the
+ * destination, move it to an archive. Returns the archive name on success
+ * or an empty string if it was a new file, and a wikitext-formatted
+ * WikiError object on failure.
*
* The archive name should be passed through to recordUpload for database
* registration.
*
* @param string $sourcePath Local filesystem path to the source image
* @param integer $flags A bitwise combination of:
- * File::DELETE_SOURCE Delete the source file, i.e. move
+ * File::DELETE_SOURCE Delete the source file, i.e. move
* rather than copy
- * @return The archive name on success or an empty string if it was a new
- * file, and a wikitext-formatted WikiError object on failure.
+ * @return The archive name on success or an empty string if it was a new
+ * file, and a wikitext-formatted WikiError object on failure.
*
* STUB
* Overridden by LocalFile
@@ -829,18 +868,19 @@ abstract class File {
} else {
$db = wfGetDB( DB_SLAVE );
}
- $linkCache =& LinkCache::singleton();
+ $linkCache = LinkCache::singleton();
list( $page, $imagelinks ) = $db->tableNamesN( 'page', 'imagelinks' );
$encName = $db->addQuotes( $this->getName() );
- $sql = "SELECT page_namespace,page_title,page_id FROM $page,$imagelinks WHERE page_id=il_from AND il_to=$encName $options";
+ $sql = "SELECT page_namespace,page_title,page_id,page_len,page_is_redirect,
+ FROM $page,$imagelinks WHERE page_id=il_from AND il_to=$encName $options";
$res = $db->query( $sql, __METHOD__ );
$retVal = array();
if ( $db->numRows( $res ) ) {
while ( $row = $db->fetchObject( $res ) ) {
- if ( $titleObj = Title::makeTitle( $row->page_namespace, $row->page_title ) ) {
- $linkCache->addGoodLinkObj( $row->page_id, $titleObj );
+ if ( $titleObj = Title::newFromRow( $row ) ) {
+ $linkCache->addGoodLinkObj( $row->page_id, $titleObj, $row->page_len, $row->page_is_redirect );
$retVal[] = $titleObj;
}
}
@@ -862,8 +902,8 @@ abstract class File {
*
* @return bool
*/
- function isLocal() {
- return $this->getRepoName() == 'local';
+ function isLocal() {
+ return $this->getRepoName() == 'local';
}
/**
@@ -871,8 +911,14 @@ abstract class File {
*
* @return string
*/
- function getRepoName() {
- return $this->repo ? $this->repo->getName() : 'unknown';
+ function getRepoName() {
+ return $this->repo ? $this->repo->getName() : 'unknown';
+ }
+ /*
+ * Returns the repository
+ */
+ function getRepo() {
+ return $this->repo;
}
/**
@@ -902,6 +948,22 @@ abstract class File {
}
/**
+ * Move file to the new title
+ *
+ * Move current, old version and all thumbnails
+ * to the new filename. Old file is deleted.
+ *
+ * Cache purging is done; checks for validity
+ * and logging are caller's responsibility
+ *
+ * @param $target Title New file name
+ * @return FileRepoStatus object.
+ */
+ function move( $target ) {
+ $this->readOnlyError();
+ }
+
+ /**
* Delete all versions of the file.
*
* Moves the files into an archive directory (or deletes them)
@@ -910,11 +972,12 @@ abstract class File {
* Cache purging is done; logging is caller's responsibility.
*
* @param $reason
+ * @param $suppress, hide content from sysops?
* @return true on success, false on some kind of failure
* STUB
* Overridden by LocalFile
*/
- function delete( $reason ) {
+ function delete( $reason, $suppress = false ) {
$this->readOnlyError();
}
@@ -926,12 +989,13 @@ abstract class File {
*
* @param $versions set of record ids of deleted items to restore,
* or empty to restore all revisions.
+ * @param $unsuppress, remove restrictions on content upon restoration?
* @return the number of file revisions restored if successful,
* or false on failure
* STUB
* Overridden by LocalFile
*/
- function restore( $versions=array(), $Unsuppress=false ) {
+ function restore( $versions=array(), $unsuppress=false ) {
$this->readOnlyError();
}
@@ -971,9 +1035,9 @@ abstract class File {
return round( $srcHeight * $dstWidth / $srcWidth );
}
}
-
+
/**
- * Get an image size array like that returned by getimagesize(), or false if it
+ * Get an image size array like that returned by getimagesize(), or false if it
* can't be determined.
*
* @param string $fileName The filename
@@ -998,13 +1062,26 @@ abstract class File {
* Get the HTML text of the description page, if available
*/
function getDescriptionText() {
+ global $wgMemc;
if ( !$this->repo->fetchDescription ) {
return false;
}
$renderUrl = $this->repo->getDescriptionRenderUrl( $this->getName() );
if ( $renderUrl ) {
+ if ( $this->repo->descriptionCacheExpiry > 0 ) {
+ wfDebug("Attempting to get the description from cache...");
+ $key = wfMemcKey( 'RemoteFileDescription', 'url', md5($renderUrl) );
+ $obj = $wgMemc->get($key);
+ if ($obj) {
+ wfDebug("success!\n");
+ return $obj;
+ }
+ wfDebug("miss\n");
+ }
wfDebug( "Fetching shared description from $renderUrl\n" );
- return Http::get( $renderUrl );
+ $res = Http::get( $renderUrl );
+ if ( $res && $this->repo->descriptionCacheExpiry > 0 ) $wgMemc->set( $key, $res, $this->repo->descriptionCacheExpiry );
+ return $res;
} else {
return false;
}
@@ -1020,14 +1097,14 @@ abstract class File {
/**
* Get the 14-character timestamp of the file upload, or false if
- * it doesn't exist
+ * it doesn't exist
*/
function getTimestamp() {
$path = $this->getPath();
if ( !file_exists( $path ) ) {
return false;
}
- return wfTimestamp( filemtime( $path ) );
+ return wfTimestamp( TS_MW, filemtime( $path ) );
}
/**
@@ -1036,12 +1113,12 @@ abstract class File {
function getSha1() {
return self::sha1Base36( $this->getPath() );
}
-
+
/**
* Determine if the current user is allowed to view a particular
* field of this file, if it's marked as deleted.
* STUB
- * @param int $field
+ * @param int $field
* @return bool
*/
function userCan( $field ) {
@@ -1052,13 +1129,13 @@ abstract class File {
* Get an associative array containing information about a file in the local filesystem.
*
* @param string $path Absolute local filesystem path
- * @param mixed $ext The file extension, or true to extract it from the filename.
+ * @param mixed $ext The file extension, or true to extract it from the filename.
* Set it to false to ignore the extension.
*/
static function getPropsFromPath( $path, $ext = true ) {
wfProfileIn( __METHOD__ );
wfDebug( __METHOD__.": Getting file info for $path\n" );
- $info = array(
+ $info = array(
'fileExists' => file_exists( $path ) && !is_dir( $path )
);
$gis = false;
@@ -1111,8 +1188,8 @@ abstract class File {
}
/**
- * Get a SHA-1 hash of a file in the local filesystem, in base-36 lower case
- * encoding, zero padded to 31 digits.
+ * Get a SHA-1 hash of a file in the local filesystem, in base-36 lower case
+ * encoding, zero padded to 31 digits.
*
* 160 log 2 / log 36 = 30.95, so the 160-bit hash fills 31 digits in base 36
* fairly neatly.
@@ -1160,6 +1237,14 @@ abstract class File {
function getRedirected() {
return $this->redirected;
}
+
+ function getRedirectedTitle() {
+ if ( $this->redirected ) {
+ if ( !$this->redirectTitle )
+ $this->redirectTitle = Title::makeTitle( NS_IMAGE, $this->redirected );
+ return $this->redirectTitle;
+ }
+ }
function redirectedFrom( $from ) {
$this->redirected = $from;
@@ -1172,5 +1257,3 @@ define( 'MW_IMG_DELETED_FILE', File::DELETED_FILE );
define( 'MW_IMG_DELETED_COMMENT', File::DELETED_COMMENT );
define( 'MW_IMG_DELETED_USER', File::DELETED_USER );
define( 'MW_IMG_DELETED_RESTRICTED', File::DELETED_RESTRICTED );
-
-
diff --git a/includes/filerepo/FileRepo.php b/includes/filerepo/FileRepo.php
index ee7691a6..edfc2a99 100644
--- a/includes/filerepo/FileRepo.php
+++ b/includes/filerepo/FileRepo.php
@@ -3,9 +3,12 @@
/**
* Base class for file repositories
* Do not instantiate, use a derived class.
+ * @ingroup FileRepo
*/
abstract class FileRepo {
const DELETE_SOURCE = 1;
+ const FIND_PRIVATE = 1;
+ const FIND_IGNORE_REDIRECT = 2;
const OVERWRITE = 2;
const OVERWRITE_SAME = 4;
@@ -13,20 +16,21 @@ abstract class FileRepo {
var $descBaseUrl, $scriptDirUrl, $articleUrl, $fetchDescription, $initialCapital;
var $pathDisclosureProtection = 'paranoid';
- /**
+ /**
* Factory functions for creating new files
* Override these in the base class
*/
var $fileFactory = false, $oldFileFactory = false;
+ var $fileFactoryKey = false, $oldFileFactoryKey = false;
function __construct( $info ) {
// Required settings
$this->name = $info['name'];
-
+
// Optional settings
$this->initialCapital = true; // by default
- foreach ( array( 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription',
- 'thumbScriptUrl', 'initialCapital', 'pathDisclosureProtection' ) as $var )
+ foreach ( array( 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription',
+ 'thumbScriptUrl', 'initialCapital', 'pathDisclosureProtection', 'descriptionCacheExpiry' ) as $var )
{
if ( isset( $info[$var] ) ) {
$this->$var = $info[$var];
@@ -45,10 +49,10 @@ abstract class FileRepo {
/**
* Create a new File object from the local repository
* @param mixed $title Title object or string
- * @param mixed $time Time at which the image is supposed to have existed.
- * If this is specified, the returned object will be an
+ * @param mixed $time Time at which the image was uploaded.
+ * If this is specified, the returned object will be an
* instance of the repository's old file class instead of
- * a current file. Repositories not supporting version
+ * a current file. Repositories not supporting version
* control should return false if this parameter is set.
*/
function newFile( $title, $time = false ) {
@@ -70,13 +74,20 @@ abstract class FileRepo {
}
/**
- * Find an instance of the named file that existed at the specified time
- * Returns false if the file did not exist. Repositories not supporting
+ * Find an instance of the named file created at the specified time
+ * Returns false if the file does not exist. Repositories not supporting
* version control should return false if the time is specified.
*
+ * @param mixed $title Title object or string
* @param mixed $time 14-character timestamp, or false for the current version
*/
- function findFile( $title, $time = false ) {
+ function findFile( $title, $time = false, $flags = 0 ) {
+ if ( !($title instanceof Title) ) {
+ $title = Title::makeTitleSafe( NS_IMAGE, $title );
+ if ( !is_object( $title ) ) {
+ return false;
+ }
+ }
# First try the current version of the file to see if it precedes the timestamp
$img = $this->newFile( $title );
if ( !$img ) {
@@ -86,23 +97,100 @@ abstract class FileRepo {
return $img;
}
# Now try an old version of the file
- $img = $this->newFile( $title, $time );
- if ( $img->exists() ) {
- return $img;
+ if ( $time !== false ) {
+ $img = $this->newFile( $title, $time );
+ if ( $img->exists() ) {
+ if ( !$img->isDeleted(File::DELETED_FILE) ) {
+ return $img;
+ } else if ( ($flags & FileRepo::FIND_PRIVATE) && $img->userCan(File::DELETED_FILE) ) {
+ return $img;
+ }
+ }
}
-
+
# Now try redirects
- $redir = $this->checkRedirect( $title );
+ if ( $flags & FileRepo::FIND_IGNORE_REDIRECT ) {
+ return false;
+ }
+ $redir = $this->checkRedirect( $title );
if( $redir && $redir->getNamespace() == NS_IMAGE) {
$img = $this->newFile( $redir );
if( !$img ) {
return false;
}
if( $img->exists() ) {
- $img->redirectedFrom( $title->getText() );
+ $img->redirectedFrom( $title->getDBkey() );
return $img;
}
}
+ return false;
+ }
+
+ /*
+ * Find many files at once.
+ * @param array $titles, an array of titles
+ * @param int $flags
+ */
+ function findFiles( $titles, $flags ) {
+ $result = array();
+ foreach ( $titles as $index => $title ) {
+ $file = $this->findFile( $title, $flags );
+ if ( $file )
+ $result[$file->getTitle()->getDBkey()] = $file;
+ }
+ return $result;
+ }
+
+ /**
+ * Create a new File object from the local repository
+ * @param mixed $sha1 SHA-1 key
+ * @param mixed $time Time at which the image was uploaded.
+ * If this is specified, the returned object will be an
+ * instance of the repository's old file class instead of
+ * a current file. Repositories not supporting version
+ * control should return false if this parameter is set.
+ */
+ function newFileFromKey( $sha1, $time = false ) {
+ if ( $time ) {
+ if ( $this->oldFileFactoryKey ) {
+ return call_user_func( $this->oldFileFactoryKey, $sha1, $this, $time );
+ } else {
+ return false;
+ }
+ } else {
+ return call_user_func( $this->fileFactoryKey, $sha1, $this );
+ }
+ }
+
+ /**
+ * Find an instance of the file with this key, created at the specified time
+ * Returns false if the file does not exist. Repositories not supporting
+ * version control should return false if the time is specified.
+ *
+ * @param string $sha1 string
+ * @param mixed $time 14-character timestamp, or false for the current version
+ */
+ function findFileFromKey( $sha1, $time = false, $flags = 0 ) {
+ # First try the current version of the file to see if it precedes the timestamp
+ $img = $this->newFileFromKey( $sha1 );
+ if ( !$img ) {
+ return false;
+ }
+ if ( $img->exists() && ( !$time || $img->getTimestamp() == $time ) ) {
+ return $img;
+ }
+ # Now try an old version of the file
+ if ( $time !== false ) {
+ $img = $this->newFileFromKey( $sha1, $time );
+ if ( $img->exists() ) {
+ if ( !$img->isDeleted(File::DELETED_FILE) ) {
+ return $img;
+ } else if ( ($flags & FileRepo::FIND_PRIVATE) && $img->userCan(File::DELETED_FILE) ) {
+ return $img;
+ }
+ }
+ }
+ return false;
}
/**
@@ -163,11 +251,11 @@ abstract class FileRepo {
function getDescBaseUrl() {
if ( is_null( $this->descBaseUrl ) ) {
if ( !is_null( $this->articleUrl ) ) {
- $this->descBaseUrl = str_replace( '$1',
- wfUrlencode( Namespace::getCanonicalName( NS_IMAGE ) ) . ':', $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( Namespace::getCanonicalName( NS_IMAGE ) ) . ':';
+ $this->descBaseUrl = $this->scriptDirUrl . '/index.php?title=' .
+ wfUrlencode( MWNamespace::getCanonicalName( NS_IMAGE ) ) . ':';
} else {
$this->descBaseUrl = false;
}
@@ -177,8 +265,8 @@ abstract class FileRepo {
/**
* 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
+ * unknown or not applicable. In general this should only be called by the
+ * File class, since it may return invalid results for certain kinds of
* repositories. Use File::getDescriptionUrl() in user code.
*
* In particular, it uses the article paths as specified to the repository
@@ -194,15 +282,15 @@ abstract class FileRepo {
}
/**
- * Get the URL of the content-only fragment of the description page. For
- * MediaWiki this means action=render. This should only be called by the
- * repository's file class, since it may return invalid results. User code
+ * Get the URL of the content-only fragment of the description page. For
+ * MediaWiki this means action=render. This should only be called by the
+ * repository's file class, since it may return invalid results. User code
* should use File::getDescriptionText().
*/
function getDescriptionRenderUrl( $name ) {
if ( isset( $this->scriptDirUrl ) ) {
- return $this->scriptDirUrl . '/index.php?title=' .
- wfUrlencode( Namespace::getCanonicalName( NS_IMAGE ) . ':' . $name ) .
+ return $this->scriptDirUrl . '/index.php?title=' .
+ wfUrlencode( MWNamespace::getCanonicalName( NS_IMAGE ) . ':' . $name ) .
'&action=render';
} else {
$descBase = $this->getDescBaseUrl();
@@ -223,7 +311,7 @@ abstract class FileRepo {
* @param integer $flags Bitwise combination of the following flags:
* self::DELETE_SOURCE Delete the source file after upload
* self::OVERWRITE Overwrite an existing destination file instead of failing
- * self::OVERWRITE_SAME Overwrite the file if the destination exists and has the
+ * self::OVERWRITE_SAME Overwrite the file if the destination exists and has the
* same contents as the source
* @return FileRepoStatus
*/
@@ -247,7 +335,7 @@ abstract class FileRepo {
* Pick a random name in the temp zone and store a file to it.
* Returns a FileRepoStatus object with the URL in the value.
*
- * @param string $originalName The base name of the file as specified
+ * @param string $originalName The base name of the file as specified
* by the user. The file extension will be maintained.
* @param string $srcPath The current location of the file.
*/
@@ -268,7 +356,7 @@ abstract class FileRepo {
* virtual URL, into this repository at the specified destination location.
*
* Returns a FileRepoStatus object. On success, the value contains "new" or
- * "archived", to indicate whether the file was new with that name.
+ * "archived", to indicate whether the file was new with that name.
*
* @param string $srcPath The source path or URL
* @param string $dstRel The destination relative path
@@ -301,16 +389,16 @@ abstract class FileRepo {
/**
* Move a group of files to the deletion archive.
*
- * If no valid deletion archive is configured, this may either delete the
+ * If no valid deletion archive is configured, this may either delete the
* file or throw an exception, depending on the preference of the repository.
*
* The overwrite policy is determined by the repository -- currently FSRepo
- * assumes a naming scheme in the deleted zone based on content hash, as
+ * assumes a naming scheme in the deleted zone based on content hash, as
* opposed to the public zone which is assumed to be unique.
*
- * @param array $sourceDestPairs Array of source/destination pairs. Each element
+ * @param array $sourceDestPairs Array of source/destination pairs. Each element
* is a two-element array containing the source file path relative to the
- * public root in the first element, and the archive file path relative
+ * public root in the first element, and the archive file path relative
* to the deleted zone root in the second element.
* @return FileRepoStatus
*/
@@ -318,10 +406,10 @@ abstract class FileRepo {
/**
* Move a file to the deletion archive.
- * If no valid deletion archive exists, this may either delete the file
+ * If no valid deletion archive exists, this may either delete the file
* or throw an exception, depending on the preference of the repository
* @param mixed $srcRel Relative path for the file to be deleted
- * @param mixed $archiveRel Relative path for the archive location.
+ * @param mixed $archiveRel Relative path for the archive location.
* Relative to a private archive directory.
* @return WikiError object (wikitext-formatted), or true for success
*/
@@ -423,5 +511,17 @@ abstract class FileRepo {
function checkRedirect( $title ) {
return false;
}
-}
+ /**
+ * Invalidates image redirect cache related to that image
+ * STUB
+ *
+ * @param Title $title Title of image
+ */
+ function invalidateImageRedirect( $title ) {
+ }
+
+ function findBySha1( $hash ) {
+ return array();
+ }
+}
diff --git a/includes/filerepo/FileRepoStatus.php b/includes/filerepo/FileRepoStatus.php
index 5dd1dbda..63460fa8 100644
--- a/includes/filerepo/FileRepoStatus.php
+++ b/includes/filerepo/FileRepoStatus.php
@@ -1,23 +1,14 @@
<?php
/**
- * Generic operation result class
- * Has warning/error list, boolean status and arbitrary value
+ * Generic operation result class for FileRepo-related operations
+ * @ingroup FileRepo
*/
-class FileRepoStatus {
- var $ok = true;
- var $value;
-
- /** Counters for batch operations */
- var $successCount = 0, $failCount = 0;
-
- /*semi-private*/ var $errors = array();
- /*semi-private*/ var $cleanCallback = false;
-
+class FileRepoStatus extends Status {
/**
* Factory function for fatal errors
*/
- static function newFatal( $repo, $message /*, parameters...*/ ) {
+ static function newFatal( $repo /*, parameters...*/ ) {
$params = array_slice( func_get_args(), 1 );
$result = new self( $repo );
call_user_func_array( array( &$result, 'error' ), $params );
@@ -30,142 +21,10 @@ class FileRepoStatus {
$result->value = $value;
return $result;
}
-
+
function __construct( $repo = false ) {
if ( $repo ) {
$this->cleanCallback = $repo->getErrorCleanupFunction();
}
}
-
- function setResult( $ok, $value = null ) {
- $this->ok = $ok;
- $this->value = $value;
- }
-
- function isGood() {
- return $this->ok && !$this->errors;
- }
-
- function isOK() {
- return $this->ok;
- }
-
- function warning( $message /*, parameters... */ ) {
- $params = array_slice( func_get_args(), 1 );
- $this->errors[] = array(
- 'type' => 'warning',
- 'message' => $message,
- 'params' => $params );
- }
-
- /**
- * Add an error, do not set fatal flag
- * This can be used for non-fatal errors
- */
- function error( $message /*, parameters... */ ) {
- $params = array_slice( func_get_args(), 1 );
- $this->errors[] = array(
- 'type' => 'error',
- 'message' => $message,
- 'params' => $params );
- }
-
- /**
- * Add an error and set OK to false, indicating that the operation as a whole was fatal
- */
- function fatal( $message /*, parameters... */ ) {
- $params = array_slice( func_get_args(), 1 );
- $this->errors[] = array(
- 'type' => 'error',
- 'message' => $message,
- 'params' => $params );
- $this->ok = false;
- }
-
- protected function cleanParams( $params ) {
- if ( !$this->cleanCallback ) {
- return $params;
- }
- $cleanParams = array();
- foreach ( $params as $i => $param ) {
- $cleanParams[$i] = call_user_func( $this->cleanCallback, $param );
- }
- return $cleanParams;
- }
-
- protected function getItemXML( $item ) {
- $params = $this->cleanParams( $item['params'] );
- $xml = "<{$item['type']}>\n" .
- Xml::element( 'message', null, $item['message'] ) . "\n" .
- Xml::element( 'text', null, wfMsgReal( $item['message'], $params ) ) ."\n";
- foreach ( $params as $param ) {
- $xml .= Xml::element( 'param', null, $param );
- }
- $xml .= "</{$this->type}>\n";
- return $xml;
- }
-
- /**
- * Get the error list as XML
- */
- function getXML() {
- $xml = "<errors>\n";
- foreach ( $this->errors as $error ) {
- $xml .= $this->getItemXML( $error );
- }
- $xml .= "</errors>\n";
- return $xml;
- }
-
- /**
- * Get the error list as a wikitext formatted list
- * @param string $shortContext A short enclosing context message name, to be used
- * when there is a single error
- * @param string $longContext A long enclosing context message name, for a list
- */
- function getWikiText( $shortContext = false, $longContext = false ) {
- if ( count( $this->errors ) == 0 ) {
- if ( $this->ok ) {
- $this->fatal( 'internalerror_info',
- __METHOD__." called for a good result, this is incorrect\n" );
- } else {
- $this->fatal( 'internalerror_info',
- __METHOD__.": Invalid result object: no error text but not OK\n" );
- }
- }
- if ( count( $this->errors ) == 1 ) {
- $params = array_map( 'wfEscapeWikiText', $this->cleanParams( $this->errors[0]['params'] ) );
- $s = wfMsgReal( $this->errors[0]['message'], $params, true, false, false );
- if ( $shortContext ) {
- $s = wfMsgNoTrans( $shortContext, $s );
- } elseif ( $longContext ) {
- $s = wfMsgNoTrans( $longContext, "* $s\n" );
- }
- } else {
- $s = '';
- foreach ( $this->errors as $error ) {
- $params = array_map( 'wfEscapeWikiText', $this->cleanParams( $error['params'] ) );
- $s .= '* ' . wfMsgReal( $error['message'], $params, true, false, false ) . "\n";
- }
- if ( $longContext ) {
- $s = wfMsgNoTrans( $longContext, $s );
- } elseif ( $shortContext ) {
- $s = wfMsgNoTrans( $shortContext, "\n* $s\n" );
- }
- }
- return $s;
- }
-
- /**
- * Merge another status object into this one
- */
- function merge( $other, $overwriteValue = false ) {
- $this->errors = array_merge( $this->errors, $other->errors );
- $this->ok = $this->ok && $other->ok;
- if ( $overwriteValue ) {
- $this->value = $other->value;
- }
- $this->successCount += $other->successCount;
- $this->failCount += $other->failCount;
- }
}
diff --git a/includes/filerepo/ForeignAPIFile.php b/includes/filerepo/ForeignAPIFile.php
new file mode 100644
index 00000000..aaf92204
--- /dev/null
+++ b/includes/filerepo/ForeignAPIFile.php
@@ -0,0 +1,101 @@
+<?php
+
+/**
+ * Very hacky and inefficient
+ * do not use :D
+ *
+ * @ingroup FileRepo
+ */
+class ForeignAPIFile extends File {
+ function __construct( $title, $repo, $info ) {
+ parent::__construct( $title, $repo );
+ $this->mInfo = $info;
+ }
+
+ static function newFromTitle( $title, $repo ) {
+ $info = $repo->getImageInfo( $title );
+ if( $info ) {
+ return new ForeignAPIFile( $title, $repo, $info );
+ } else {
+ return null;
+ }
+ }
+
+ // Dummy functions...
+ public function exists() {
+ return true;
+ }
+
+ public function getPath() {
+ return false;
+ }
+
+ function transform( $params, $flags = 0 ) {
+ $thumbUrl = $this->repo->getThumbUrl(
+ $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;
+ }
+
+ // Info we can get from API...
+ public function getWidth( $page = 1 ) {
+ return intval( @$this->mInfo['width'] );
+ }
+
+ public function getHeight( $page = 1 ) {
+ return intval( @$this->mInfo['height'] );
+ }
+
+ public function getMetadata() {
+ return serialize( (array)@$this->mInfo['metadata'] );
+ }
+
+ public function getSize() {
+ return intval( @$this->mInfo['size'] );
+ }
+
+ public function getUrl() {
+ return strval( @$this->mInfo['url'] );
+ }
+
+ public function getUser( $method='text' ) {
+ return strval( @$this->mInfo['user'] );
+ }
+
+ public function getDescription() {
+ return strval( @$this->mInfo['comment'] );
+ }
+
+ function getSha1() {
+ return wfBaseConvert( strval( @$this->mInfo['sha1'] ), 16, 36, 31 );
+ }
+
+ function getTimestamp() {
+ return wfTimestamp( TS_MW, strval( @$this->mInfo['timestamp'] ) );
+ }
+
+ function getMimeType() {
+ if( empty( $info['mime'] ) ) {
+ $magic = MimeMagic::singleton();
+ $info['mime'] = $magic->guessTypesForExtension( $this->getExtension() );
+ }
+ return $info['mime'];
+ }
+
+ /// @fixme May guess wrong on file types that can be eg audio or video
+ function getMediaType() {
+ $magic = MimeMagic::singleton();
+ return $magic->getMediaType( null, $this->getMimeType() );
+ }
+
+ function getDescriptionUrl() {
+ return isset( $this->mInfo['descriptionurl'] )
+ ? $this->mInfo['descriptionurl']
+ : false;
+ }
+}
diff --git a/includes/filerepo/ForeignAPIRepo.php b/includes/filerepo/ForeignAPIRepo.php
new file mode 100644
index 00000000..0dee699f
--- /dev/null
+++ b/includes/filerepo/ForeignAPIRepo.php
@@ -0,0 +1,110 @@
+<?php
+
+/**
+ * A foreign repository with a remote MediaWiki with an API thingy
+ * Very hacky and inefficient
+ * do not use except for testing :D
+ *
+ * Example config:
+ *
+ * $wgForeignFileRepos[] = array(
+ * 'class' => 'ForeignAPIRepo',
+ * 'name' => 'shared',
+ * 'apibase' => 'http://en.wikipedia.org/w/api.php',
+ * 'fetchDescription' => true, // Optional
+ * 'descriptionCacheExpiry' => 3600,
+ * );
+ *
+ * @ingroup FileRepo
+ */
+class ForeignAPIRepo extends FileRepo {
+ var $fileFactory = array( 'ForeignAPIFile', 'newFromTitle' );
+ protected $mQueryCache = array();
+
+ function __construct( $info ) {
+ parent::__construct( $info );
+ $this->mApiBase = $info['apibase']; // http://commons.wikimedia.org/w/api.php
+ if( !$this->scriptDirUrl ) {
+ // hack for description fetches
+ $this->scriptDirUrl = dirname( $this->mApiBase );
+ }
+ }
+
+ function storeBatch( $triplets, $flags = 0 ) {
+ return false;
+ }
+
+ function storeTemp( $originalName, $srcPath ) {
+ return false;
+ }
+ function publishBatch( $triplets, $flags = 0 ) {
+ return false;
+ }
+ function deleteBatch( $sourceDestPairs ) {
+ return false;
+ }
+ function getFileProps( $virtualUrl ) {
+ return false;
+ }
+
+ protected function queryImage( $query ) {
+ $data = $this->fetchImageQuery( $query );
+
+ if( isset( $data['query']['pages'] ) ) {
+ foreach( $data['query']['pages'] as $pageid => $info ) {
+ if( isset( $info['imageinfo'][0] ) ) {
+ return $info['imageinfo'][0];
+ }
+ }
+ }
+ return false;
+ }
+
+ protected function fetchImageQuery( $query ) {
+ global $wgMemc;
+
+ $url = $this->mApiBase .
+ '?' .
+ wfArrayToCgi(
+ array_merge( $query,
+ array(
+ 'format' => 'json',
+ 'action' => 'query',
+ 'prop' => 'imageinfo' ) ) );
+
+ if( !isset( $this->mQueryCache[$url] ) ) {
+ $key = wfMemcKey( 'ForeignAPIRepo', $url );
+ $data = $wgMemc->get( $key );
+ if( !$data ) {
+ $data = Http::get( $url );
+ $wgMemc->set( $key, $data, 3600 );
+ }
+
+ if( count( $this->mQueryCache ) > 100 ) {
+ // Keep the cache from growing infinitely
+ $this->mQueryCache = array();
+ }
+ $this->mQueryCache[$url] = $data;
+ }
+ return json_decode( $this->mQueryCache[$url], true );
+ }
+
+ function getImageInfo( $title, $time = false ) {
+ return $this->queryImage( array(
+ 'titles' => 'Image:' . $title->getText(),
+ 'iiprop' => 'timestamp|user|comment|url|size|sha1|metadata|mime' ) );
+ }
+
+ function getThumbUrl( $name, $width=-1, $height=-1 ) {
+ $info = $this->queryImage( array(
+ 'titles' => 'Image:' . $name,
+ 'iiprop' => 'url',
+ 'iiurlwidth' => $width,
+ 'iiurlheight' => $height ) );
+ if( $info ) {
+ return $info['thumburl'];
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/includes/filerepo/ForeignDBFile.php b/includes/filerepo/ForeignDBFile.php
index 4d11640a..eed26048 100644
--- a/includes/filerepo/ForeignDBFile.php
+++ b/includes/filerepo/ForeignDBFile.php
@@ -1,34 +1,52 @@
<?php
+/**
+ * @ingroup FileRepo
+ */
class ForeignDBFile extends LocalFile {
- static function newFromTitle( $title, $repo ) {
+ static function newFromTitle( $title, $repo, $unused = null ) {
return new self( $title, $repo );
}
+ /**
+ * Create a ForeignDBFile from a title
+ * Do not call this except from inside a repo class.
+ */
+ static function newFromRow( $row, $repo ) {
+ $title = Title::makeTitle( NS_IMAGE, $row->img_name );
+ $file = new self( $title, $repo );
+ $file->loadFromRow( $row );
+ return $file;
+ }
+
function getCacheKey() {
if ( $this->repo->hasSharedCache ) {
$hashedName = md5($this->name);
- return wfForeignMemcKey( $this->repo->dbName, $this->repo->tablePrefix,
+ return wfForeignMemcKey( $this->repo->dbName, $this->repo->tablePrefix,
'file', $hashedName );
} else {
return false;
}
}
- function publish( /*...*/ ) {
+ function publish( $srcPath, $flags = 0 ) {
$this->readOnlyError();
}
- function recordUpload( /*...*/ ) {
+ function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
+ $watch = false, $timestamp = false ) {
$this->readOnlyError();
}
- function restore( /*...*/ ) {
+ function restore( $versions = array(), $unsuppress = false ) {
$this->readOnlyError();
}
- function delete( /*...*/ ) {
+ function delete( $reason, $suppress = false ) {
$this->readOnlyError();
}
-
+ function move( $target ) {
+ $this->readOnlyError();
+ }
+
function getDescriptionUrl() {
// Restore remote behaviour
return File::getDescriptionUrl();
@@ -39,4 +57,3 @@ class ForeignDBFile extends LocalFile {
return File::getDescriptionText();
}
}
-
diff --git a/includes/filerepo/ForeignDBRepo.php b/includes/filerepo/ForeignDBRepo.php
index 13dcd029..e078dd25 100644
--- a/includes/filerepo/ForeignDBRepo.php
+++ b/includes/filerepo/ForeignDBRepo.php
@@ -2,16 +2,17 @@
/**
* A foreign repository with an accessible MediaWiki database
+ * @ingroup FileRepo
*/
-
class ForeignDBRepo extends LocalRepo {
# Settings
- var $dbType, $dbServer, $dbUser, $dbPassword, $dbName, $dbFlags,
+ var $dbType, $dbServer, $dbUser, $dbPassword, $dbName, $dbFlags,
$tablePrefix, $hasSharedCache;
-
+
# Other stuff
var $dbConn;
var $fileFactory = array( 'ForeignDBFile', 'newFromTitle' );
+ var $fileFromRowFactory = array( 'ForeignDBFile', 'newFromRow' );
function __construct( $info ) {
parent::__construct( $info );
@@ -28,8 +29,8 @@ class ForeignDBRepo extends LocalRepo {
function getMasterDB() {
if ( !isset( $this->dbConn ) ) {
$class = 'Database' . ucfirst( $this->dbType );
- $this->dbConn = new $class( $this->dbServer, $this->dbUser,
- $this->dbPassword, $this->dbName, false, $this->dbFlags,
+ $this->dbConn = new $class( $this->dbServer, $this->dbUser,
+ $this->dbPassword, $this->dbName, false, $this->dbFlags,
$this->tablePrefix );
}
return $this->dbConn;
@@ -53,5 +54,3 @@ class ForeignDBRepo extends LocalRepo {
throw new MWException( get_class($this) . ': write operations are not supported' );
}
}
-
-
diff --git a/includes/filerepo/ForeignDBViaLBRepo.php b/includes/filerepo/ForeignDBViaLBRepo.php
new file mode 100644
index 00000000..13c9f434
--- /dev/null
+++ b/includes/filerepo/ForeignDBViaLBRepo.php
@@ -0,0 +1,39 @@
+<?php
+
+/**
+ * A foreign repository with a MediaWiki database accessible via the configured LBFactory
+ * @ingroup FileRepo
+ */
+class ForeignDBViaLBRepo extends LocalRepo {
+ var $wiki, $dbName, $tablePrefix;
+ var $fileFactory = array( 'ForeignDBFile', 'newFromTitle' );
+ var $fileFromRowFactory = array( 'ForeignDBFile', 'newFromRow' );
+
+ function __construct( $info ) {
+ parent::__construct( $info );
+ $this->wiki = $info['wiki'];
+ list( $this->dbName, $this->tablePrefix ) = wfSplitWikiID( $this->wiki );
+ $this->hasSharedCache = $info['hasSharedCache'];
+ }
+
+ function getMasterDB() {
+ return wfGetDB( DB_MASTER, array(), $this->wiki );
+ }
+
+ function getSlaveDB() {
+ return wfGetDB( DB_SLAVE, array(), $this->wiki );
+ }
+ function hasSharedCache() {
+ return $this->hasSharedCache;
+ }
+
+ function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) {
+ throw new MWException( get_class($this) . ': write operations are not supported' );
+ }
+ function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) {
+ throw new MWException( get_class($this) . ': write operations are not supported' );
+ }
+ function deleteBatch( $fileMap ) {
+ throw new MWException( get_class($this) . ': write operations are not supported' );
+ }
+}
diff --git a/includes/filerepo/Image.php b/includes/filerepo/Image.php
new file mode 100644
index 00000000..665dd4bf
--- /dev/null
+++ b/includes/filerepo/Image.php
@@ -0,0 +1,74 @@
+<?php
+
+/**
+ * Backwards compatibility class
+ * @deprecated
+ * @ingroup FileRepo
+ */
+class Image extends LocalFile {
+ function __construct( $title ) {
+ wfDeprecated( __METHOD__ );
+ $repo = RepoGroup::singleton()->getLocalRepo();
+ parent::__construct( $title, $repo );
+ }
+
+ /**
+ * Wrapper for wfFindFile(), for backwards-compatibility only
+ * Do not use in core code.
+ * @deprecated
+ */
+ static function newFromTitle( $title, $time = false ) {
+ wfDeprecated( __METHOD__ );
+ $img = wfFindFile( $title, $time );
+ if ( !$img ) {
+ $img = wfLocalFile( $title );
+ }
+ return $img;
+ }
+
+ /**
+ * Wrapper for wfFindFile(), for backwards-compatibility only.
+ * Do not use in core code.
+ *
+ * @param string $name name of the image, used to create a title object using Title::makeTitleSafe
+ * @return image object or null if invalid title
+ * @deprecated
+ */
+ static function newFromName( $name ) {
+ wfDeprecated( __METHOD__ );
+ $title = Title::makeTitleSafe( NS_IMAGE, $name );
+ if ( is_object( $title ) ) {
+ $img = wfFindFile( $title );
+ if ( !$img ) {
+ $img = wfLocalFile( $title );
+ }
+ return $img;
+ } else {
+ return NULL;
+ }
+ }
+
+ /**
+ * Return the URL of an image, provided its name.
+ *
+ * Backwards-compatibility for extensions.
+ * Note that fromSharedDirectory will only use the shared path for files
+ * that actually exist there now, and will return local paths otherwise.
+ *
+ * @param string $name Name of the image, without the leading "Image:"
+ * @param boolean $fromSharedDirectory Should this be in $wgSharedUploadPath?
+ * @return string URL of $name image
+ * @deprecated
+ */
+ static function imageUrl( $name, $fromSharedDirectory = false ) {
+ wfDeprecated( __METHOD__ );
+ $image = null;
+ if( $fromSharedDirectory ) {
+ $image = wfFindFile( $name );
+ }
+ if( !$image ) {
+ $image = wfLocalFile( $name );
+ }
+ return $image->getUrl();
+ }
+}
diff --git a/includes/filerepo/LocalFile.php b/includes/filerepo/LocalFile.php
index 9b06fe2d..57c0703d 100644
--- a/includes/filerepo/LocalFile.php
+++ b/includes/filerepo/LocalFile.php
@@ -5,7 +5,7 @@
/**
* Bump this number when serialized cache records may be incompatible.
*/
-define( 'MW_FILE_VERSION', 7 );
+define( 'MW_FILE_VERSION', 8 );
/**
* Class to represent a local file in the wiki's own database
@@ -14,7 +14,7 @@ define( 'MW_FILE_VERSION', 7 );
* to generate image thumbnails or for uploading.
*
* Note that only the repo object knows what its file class is called. You should
- * never name a file class explictly outside of the repo class. Instead use the
+ * never name a file class explictly outside of the repo class. Instead use the
* repo's factory functions to generate file objects, for example:
*
* RepoGroup::singleton()->getLocalRepo()->newFile($title);
@@ -22,7 +22,7 @@ define( 'MW_FILE_VERSION', 7 );
* The convenience functions wfLocalFile() and wfFindFile() should be sufficient
* in most cases.
*
- * @addtogroup FileRepo
+ * @ingroup FileRepo
*/
class LocalFile extends File
{
@@ -48,15 +48,18 @@ class LocalFile extends File
$description, # Description of current revision of the file
$dataLoaded, # Whether or not all this has been loaded from the database (loadFromXxx)
$upgraded, # Whether the row was upgraded on load
- $locked; # True if the image row is locked
+ $locked, # True if the image row is locked
+ $deleted; # Bitfield akin to rev_deleted
/**#@-*/
/**
* Create a LocalFile from a title
* Do not call this except from inside a repo class.
+ *
+ * Note: $unused param is only here to avoid an E_STRICT
*/
- static function newFromTitle( $title, $repo ) {
+ static function newFromTitle( $title, $repo, $unused = null ) {
return new self( $title, $repo );
}
@@ -70,6 +73,48 @@ class LocalFile extends File
$file->loadFromRow( $row );
return $file;
}
+
+ /**
+ * Create a LocalFile from a SHA-1 key
+ * Do not call this except from inside a repo class.
+ */
+ static function newFromKey( $sha1, $repo, $timestamp = false ) {
+ # Polymorphic function name to distinguish foreign and local fetches
+ $fname = get_class( $this ) . '::' . __FUNCTION__;
+
+ $conds = array( 'img_sha1' => $sha1 );
+ if( $timestamp ) {
+ $conds['img_timestamp'] = $timestamp;
+ }
+ $row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ), $conds, $fname );
+ if( $row ) {
+ return self::newFromRow( $row, $repo );
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Fields in the image table
+ */
+ static function selectFields() {
+ return array(
+ 'img_name',
+ 'img_size',
+ 'img_width',
+ 'img_height',
+ 'img_metadata',
+ 'img_bits',
+ 'img_media_type',
+ 'img_major_mime',
+ 'img_minor_mime',
+ 'img_description',
+ 'img_user',
+ 'img_user_text',
+ 'img_timestamp',
+ 'img_sha1',
+ );
+ }
/**
* Constructor.
@@ -156,7 +201,7 @@ class LocalFile extends File
}
function getCacheFields( $prefix = 'img_' ) {
- static $fields = array( 'size', 'width', 'height', 'bits', 'media_type',
+ static $fields = array( 'size', 'width', 'height', 'bits', 'media_type',
'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1', 'user', 'user_text', 'description' );
static $results = array();
if ( $prefix == '' ) {
@@ -197,7 +242,7 @@ class LocalFile extends File
}
/**
- * Decode a row from the database (either object or array) to an array
+ * Decode a row from the database (either object or array) to an array
* with timestamps and MIME types decoded, and the field prefix removed.
*/
function decodeRow( $row, $prefix = 'img_' ) {
@@ -235,7 +280,6 @@ class LocalFile extends File
$this->$name = $value;
}
$this->fileExists = true;
- // Check for rows from a previous schema, quietly upgrade them
$this->maybeUpgradeRow();
}
@@ -259,7 +303,7 @@ class LocalFile extends File
if ( wfReadOnly() ) {
return;
}
- if ( is_null($this->media_type) ||
+ if ( is_null($this->media_type) ||
$this->mime == 'image/svg'
) {
$this->upgradeRow();
@@ -316,10 +360,10 @@ class LocalFile extends File
}
/**
- * Set properties in this object to be equal to those given in the
+ * Set properties in this object to be equal to those given in the
* associative array $info. Only cacheable fields can be set.
- *
- * If 'mime' is given, it will be split into major_mime/minor_mime.
+ *
+ * If 'mime' is given, it will be split into major_mime/minor_mime.
* If major_mime/minor_mime are given, $this->mime will also be set.
*/
function setProps( $info ) {
@@ -345,6 +389,7 @@ class LocalFile extends File
/** getURL inherited */
/** getViewURL inherited */
/** getPath inherited */
+ /** isVisible inhereted */
/**
* Return the width of the image
@@ -456,7 +501,7 @@ class LocalFile extends File
/** createThumb inherited */
/** getThumbnail inherited */
/** transform inherited */
-
+
/**
* Fix thumbnail files from 1.4 or before, with extreme prejudice
*/
@@ -493,25 +538,21 @@ class LocalFile extends File
* Get all thumbnail names previously generated for this file
*/
function getThumbnails() {
- if ( $this->isHashed() ) {
- $this->load();
- $files = array();
- $dir = $this->getThumbPath();
-
- if ( is_dir( $dir ) ) {
- $handle = opendir( $dir );
-
- if ( $handle ) {
- while ( false !== ( $file = readdir($handle) ) ) {
- if ( $file{0} != '.' ) {
- $files[] = $file;
- }
+ $this->load();
+ $files = array();
+ $dir = $this->getThumbPath();
+
+ if ( is_dir( $dir ) ) {
+ $handle = opendir( $dir );
+
+ if ( $handle ) {
+ while ( false !== ( $file = readdir($handle) ) ) {
+ if ( $file{0} != '.' ) {
+ $files[] = $file;
}
- closedir( $handle );
}
+ closedir( $handle );
}
- } else {
- $files = array();
}
return $files;
@@ -547,7 +588,7 @@ class LocalFile extends File
$this->purgeThumbnails();
// Purge squid cache for this file
- wfPurgeSquidServers( array( $this->getURL() ) );
+ SquidUpdate::purge( array( $this->getURL() ) );
}
/**
@@ -571,7 +612,7 @@ class LocalFile extends File
// Purge the squid
if ( $wgUseSquid ) {
- wfPurgeSquidServers( $urls );
+ SquidUpdate::purge( $urls );
}
}
@@ -580,6 +621,9 @@ class LocalFile extends File
function getHistory($limit = null, $start = null, $end = null) {
$dbr = $this->repo->getSlaveDB();
+ $tables = array('oldimage');
+ $join_conds = array();
+ $fields = OldLocalFile::selectFields();
$conds = $opts = array();
$conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBKey() );
if( $start !== null ) {
@@ -592,14 +636,17 @@ class LocalFile extends File
$opts['LIMIT'] = $limit;
}
$opts['ORDER BY'] = 'oi_timestamp DESC';
- $res = $dbr->select('oldimage', '*', $conds, __METHOD__, $opts);
+
+ 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);
}
return $r;
}
-
+
/**
* Return the history of this file, line by line.
* starts with current version, then old versions.
@@ -617,10 +664,12 @@ class LocalFile extends File
$dbr = $this->repo->getSlaveDB();
if ( $this->historyLine == 0 ) {// called for the first time, return line from cur
- $this->historyRes = $dbr->select( 'image',
+ $this->historyRes = $dbr->select( 'image',
array(
'*',
- "'' AS oi_archive_name"
+ "'' AS oi_archive_name",
+ '0 as oi_deleted',
+ 'img_sha1'
),
array( 'img_name' => $this->title->getDBkey() ),
$fname
@@ -632,7 +681,7 @@ class LocalFile extends File
}
} else if ( $this->historyLine == 1 ) {
$dbr->freeResult($this->historyRes);
- $this->historyRes = $dbr->select( 'oldimage', '*',
+ $this->historyRes = $dbr->select( 'oldimage', '*',
array( 'oi_name' => $this->title->getDBkey() ),
$fname,
array( 'ORDER BY' => 'oi_timestamp DESC' )
@@ -681,12 +730,12 @@ class LocalFile extends File
* @param string $timestamp Timestamp for img_timestamp, or false to use the current time
*
* @return FileRepoStatus object. On success, the value member contains the
- * archive name, or an empty string if it was a new file.
+ * archive name, or an empty string if it was a new file.
*/
function upload( $srcPath, $comment, $pageText, $flags = 0, $props = false, $timestamp = false ) {
$this->lock();
$status = $this->publish( $srcPath, $flags );
- if ( $status->ok ) {
+ if ( $status->ok ) {
if ( !$this->recordUpload2( $status->value, $comment, $pageText, $props, $timestamp ) ) {
$status->fatal( 'filenotfound', $srcPath );
}
@@ -699,8 +748,8 @@ class LocalFile extends File
* Record a file upload in the upload log and the image table
* @deprecated use upload()
*/
- function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
- $watch = false, $timestamp = false )
+ function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
+ $watch = false, $timestamp = false )
{
$pageText = UploadForm::getInitialPageText( $desc, $license, $copyStatus, $source );
if ( !$this->recordUpload2( $oldver, $desc, $pageText ) ) {
@@ -717,7 +766,7 @@ 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 )
{
global $wgUser;
@@ -727,7 +776,7 @@ class LocalFile extends File
$props = $this->repo->getFileProps( $this->getVirtualUrl() );
}
$props['description'] = $comment;
- $props['user'] = $wgUser->getID();
+ $props['user'] = $wgUser->getId();
$props['user_text'] = $wgUser->getName();
$props['timestamp'] = wfTimestamp( TS_MW );
$this->setProps( $props );
@@ -735,7 +784,7 @@ class LocalFile extends File
// Delete thumbnails and refresh the metadata cache
$this->purgeThumbnails();
$this->saveToCache();
- wfPurgeSquidServers( array( $this->getURL() ) );
+ SquidUpdate::purge( array( $this->getURL() ) );
// Fail now if the file isn't there
if ( !$this->fileExists ) {
@@ -763,7 +812,7 @@ class LocalFile extends File
'img_minor_mime' => $this->minor_mime,
'img_timestamp' => $timestamp,
'img_description' => $comment,
- 'img_user' => $wgUser->getID(),
+ 'img_user' => $wgUser->getId(),
'img_user_text' => $wgUser->getName(),
'img_metadata' => $this->metadata,
'img_sha1' => $this->sha1
@@ -774,7 +823,7 @@ class LocalFile extends File
if( $dbw->affectedRows() == 0 ) {
$reupload = true;
-
+
# Collision, this is an update of a file
# Insert previous contents into oldimage
$dbw->insertSelect( 'oldimage', 'image',
@@ -793,7 +842,7 @@ class LocalFile extends File
'oi_media_type' => 'img_media_type',
'oi_major_mime' => 'img_major_mime',
'oi_minor_mime' => 'img_minor_mime',
- 'oi_sha1' => 'img_sha1',
+ 'oi_sha1' => 'img_sha1'
), array( 'img_name' => $this->getName() ), __METHOD__
);
@@ -809,7 +858,7 @@ class LocalFile extends File
'img_minor_mime' => $this->minor_mime,
'img_timestamp' => $timestamp,
'img_description' => $comment,
- 'img_user' => $wgUser->getID(),
+ 'img_user' => $wgUser->getId(),
'img_user_text' => $wgUser->getName(),
'img_metadata' => $this->metadata,
'img_sha1' => $this->sha1
@@ -836,6 +885,8 @@ class LocalFile extends File
# Create a null revision
$nullRevision = Revision::newNullRevision( $dbw, $descTitle->getArticleId(), $log->getRcComment(), false );
$nullRevision->insertOn( $dbw );
+
+ wfRunHooks( 'NewRevisionFromEditComplete', array($article, $nullRevision, false) );
$article->updateRevisionOn( $dbw, $nullRevision );
# Invalidate the cache for the description page
@@ -857,25 +908,31 @@ class LocalFile extends File
# Invalidate cache for all pages using this file
$update = new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' );
$update->doUpdate();
+ # Invalidate cache for all pages that redirects on this page
+ $redirs = $this->getTitle()->getRedirectsHere();
+ foreach( $redirs as $redir ) {
+ $update = new HTMLCacheUpdate( $redir, 'imagelinks' );
+ $update->doUpdate();
+ }
return true;
}
/**
- * Move or copy a file to its public location. If a file exists at the
- * destination, move it to an archive. Returns the archive name on success
- * or an empty string if it was a new file, and a wikitext-formatted
- * WikiError object on failure.
+ * Move or copy a file to its public location. If a file exists at the
+ * destination, move it to an archive. Returns the archive name on success
+ * or an empty string if it was a new file, and a wikitext-formatted
+ * WikiError object on failure.
*
* The archive name should be passed through to recordUpload for database
* registration.
*
* @param string $sourcePath Local filesystem path to the source image
* @param integer $flags A bitwise combination of:
- * File::DELETE_SOURCE Delete the source file, i.e. move
+ * File::DELETE_SOURCE Delete the source file, i.e. move
* rather than copy
* @return FileRepoStatus object. On success, the value member contains the
- * archive name, or an empty string if it was a new file.
+ * archive name, or an empty string if it was a new file.
*/
function publish( $srcPath, $flags = 0 ) {
$this->lock();
@@ -897,7 +954,44 @@ class LocalFile extends File
/** getExifData inherited */
/** isLocal inherited */
/** wasDeleted inherited */
-
+
+ /**
+ * Move file to the new title
+ *
+ * Move current, old version and all thumbnails
+ * to the new filename. Old file is deleted.
+ *
+ * Cache purging is done; checks for validity
+ * and logging are caller's responsibility
+ *
+ * @param $target Title New file name
+ * @return FileRepoStatus object.
+ */
+ function move( $target ) {
+ wfDebugLog( 'imagemove', "Got request to move {$this->name} to " . $target->getText() );
+ $this->lock();
+ $batch = new LocalFileMoveBatch( $this, $target );
+ $batch->addCurrent();
+ $batch->addOlds();
+
+ $status = $batch->execute();
+ wfDebugLog( 'imagemove', "Finished moving {$this->name}" );
+ $this->purgeEverything();
+ $this->unlock();
+
+ if ( $status->isOk() ) {
+ // Now switch the object
+ $this->title = $target;
+ // Force regeneration of the name and hashpath
+ unset( $this->name );
+ unset( $this->hashPath );
+ // Purge the new image
+ $this->purgeEverything();
+ }
+
+ return $status;
+ }
+
/**
* Delete all versions of the file.
*
@@ -907,11 +1001,12 @@ class LocalFile extends File
* Cache purging is done; logging is caller's responsibility.
*
* @param $reason
+ * @param $suppress
* @return FileRepoStatus object.
*/
- function delete( $reason ) {
+ function delete( $reason, $suppress = false ) {
$this->lock();
- $batch = new LocalFileDeleteBatch( $this, $reason );
+ $batch = new LocalFileDeleteBatch( $this, $reason, $suppress );
$batch->addCurrent();
# Get old version relative paths
@@ -944,12 +1039,13 @@ class LocalFile extends File
* Cache purging is done; logging is caller's responsibility.
*
* @param $reason
+ * @param $suppress
* @throws MWException or FSException on database or filestore failure
* @return FileRepoStatus object.
*/
- function deleteOld( $archiveName, $reason ) {
+ function deleteOld( $archiveName, $reason, $suppress=false ) {
$this->lock();
- $batch = new LocalFileDeleteBatch( $this, $reason );
+ $batch = new LocalFileDeleteBatch( $this, $reason, $suppress );
$batch->addOld( $archiveName );
$status = $batch->execute();
$this->unlock();
@@ -968,10 +1064,11 @@ class LocalFile extends File
*
* @param $versions set of record ids of deleted items to restore,
* or empty to restore all revisions.
+ * @param $unuppress
* @return FileRepoStatus
*/
function restore( $versions = array(), $unsuppress = false ) {
- $batch = new LocalFileRestoreBatch( $this );
+ $batch = new LocalFileRestoreBatch( $this, $unsuppress );
if ( !$versions ) {
$batch->addAll();
} else {
@@ -993,9 +1090,9 @@ class LocalFile extends File
/** pageCount inherited */
/** scaleHeight inherited */
/** getImageSize inherited */
-
+
/**
- * Get the URL of the file description page.
+ * Get the URL of the file description page.
*/
function getDescriptionUrl() {
return $this->title->getLocalUrl();
@@ -1033,7 +1130,7 @@ class LocalFile extends File
$this->sha1 = File::sha1Base36( $this->getPath() );
if ( strval( $this->sha1 ) != '' ) {
$dbw = $this->repo->getMasterDB();
- $dbw->update( 'image',
+ $dbw->update( 'image',
array( 'img_sha1' => $this->sha1 ),
array( 'img_name' => $this->getName() ),
__METHOD__ );
@@ -1059,7 +1156,7 @@ class LocalFile extends File
}
/**
- * Decrement the lock reference count. If the reference count is reduced to zero, commits
+ * Decrement the lock reference count. If the reference count is reduced to zero, commits
* the transaction and thereby releases the image lock.
*/
function unlock() {
@@ -1085,84 +1182,17 @@ class LocalFile extends File
#------------------------------------------------------------------------------
/**
- * Backwards compatibility class
- */
-class Image extends LocalFile {
- function __construct( $title ) {
- $repo = RepoGroup::singleton()->getLocalRepo();
- parent::__construct( $title, $repo );
- }
-
- /**
- * Wrapper for wfFindFile(), for backwards-compatibility only
- * Do not use in core code.
- * @deprecated
- */
- static function newFromTitle( $title, $time = false ) {
- $img = wfFindFile( $title, $time );
- if ( !$img ) {
- $img = wfLocalFile( $title );
- }
- return $img;
- }
-
- /**
- * Wrapper for wfFindFile(), for backwards-compatibility only.
- * Do not use in core code.
- *
- * @param string $name name of the image, used to create a title object using Title::makeTitleSafe
- * @return image object or null if invalid title
- * @deprecated
- */
- static function newFromName( $name ) {
- $title = Title::makeTitleSafe( NS_IMAGE, $name );
- if ( is_object( $title ) ) {
- $img = wfFindFile( $title );
- if ( !$img ) {
- $img = wfLocalFile( $title );
- }
- return $img;
- } else {
- return NULL;
- }
- }
-
- /**
- * Return the URL of an image, provided its name.
- *
- * Backwards-compatibility for extensions.
- * Note that fromSharedDirectory will only use the shared path for files
- * that actually exist there now, and will return local paths otherwise.
- *
- * @param string $name Name of the image, without the leading "Image:"
- * @param boolean $fromSharedDirectory Should this be in $wgSharedUploadPath?
- * @return string URL of $name image
- * @deprecated
- */
- static function imageUrl( $name, $fromSharedDirectory = false ) {
- $image = null;
- if( $fromSharedDirectory ) {
- $image = wfFindFile( $name );
- }
- if( !$image ) {
- $image = wfLocalFile( $name );
- }
- return $image->getUrl();
- }
-}
-
-#------------------------------------------------------------------------------
-
-/**
* Helper class for file deletion
+ * @ingroup FileRepo
*/
class LocalFileDeleteBatch {
- var $file, $reason, $srcRels = array(), $archiveUrls = array(), $deletionBatch;
+ var $file, $reason, $srcRels = array(), $archiveUrls = array(), $deletionBatch, $suppress;
var $status;
- function __construct( File $file, $reason = '' ) {
+ function __construct( File $file, $reason = '', $suppress = false ) {
$this->file = $file;
$this->reason = $reason;
+ $this->suppress = $suppress;
$this->status = $file->repo->newGood();
}
@@ -1205,7 +1235,7 @@ class LocalFileDeleteBatch {
$props = $this->file->repo->getFileProps( $oldUrl );
if ( $props['fileExists'] ) {
// Upgrade the oldimage row
- $dbw->update( 'oldimage',
+ $dbw->update( 'oldimage',
array( 'oi_sha1' => $props['sha1'] ),
array( 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ),
__METHOD__ );
@@ -1244,6 +1274,18 @@ class LocalFileDeleteBatch {
$encExt = $dbw->addQuotes( $dotExt );
list( $oldRels, $deleteCurrent ) = $this->getOldRels();
+ // Bitfields to further suppress the content
+ if ( $this->suppress ) {
+ $bitfield = 0;
+ // This should be 15...
+ $bitfield |= Revision::DELETED_TEXT;
+ $bitfield |= Revision::DELETED_COMMENT;
+ $bitfield |= Revision::DELETED_USER;
+ $bitfield |= Revision::DELETED_RESTRICTED;
+ } else {
+ $bitfield = 'oi_deleted';
+ }
+
if ( $deleteCurrent ) {
$concat = $dbw->buildConcat( array( "img_sha1", $encExt ) );
$where = array( 'img_name' => $this->file->getName() );
@@ -1254,7 +1296,7 @@ class LocalFileDeleteBatch {
'fa_deleted_user' => $encUserId,
'fa_deleted_timestamp' => $encTimestamp,
'fa_deleted_reason' => $encReason,
- 'fa_deleted' => 0,
+ 'fa_deleted' => $this->suppress ? $bitfield : 0,
'fa_name' => 'img_name',
'fa_archive_name' => 'NULL',
@@ -1278,14 +1320,14 @@ class LocalFileDeleteBatch {
$where = array(
'oi_name' => $this->file->getName(),
'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')' );
- $dbw->insertSelect( 'filearchive', 'oldimage',
+ $dbw->insertSelect( 'filearchive', 'oldimage',
array(
'fa_storage_group' => $encGroup,
'fa_storage_key' => "CASE WHEN oi_sha1='' THEN '' ELSE $concat END",
'fa_deleted_user' => $encUserId,
'fa_deleted_timestamp' => $encTimestamp,
'fa_deleted_reason' => $encReason,
- 'fa_deleted' => 0,
+ 'fa_deleted' => $this->suppress ? $bitfield : 'oi_deleted',
'fa_name' => 'oi_name',
'fa_archive_name' => 'oi_archive_name',
@@ -1300,7 +1342,8 @@ class LocalFileDeleteBatch {
'fa_description' => 'oi_description',
'fa_user' => 'oi_user',
'fa_user_text' => 'oi_user_text',
- 'fa_timestamp' => 'oi_timestamp'
+ 'fa_timestamp' => 'oi_timestamp',
+ 'fa_deleted' => $bitfield
), $where, __METHOD__ );
}
}
@@ -1309,10 +1352,10 @@ class LocalFileDeleteBatch {
$dbw = $this->file->repo->getMasterDB();
list( $oldRels, $deleteCurrent ) = $this->getOldRels();
if ( count( $oldRels ) ) {
- $dbw->delete( 'oldimage',
+ $dbw->delete( 'oldimage',
array(
'oi_name' => $this->file->getName(),
- 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')'
+ 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')'
), __METHOD__ );
}
if ( $deleteCurrent ) {
@@ -1328,15 +1371,30 @@ class LocalFileDeleteBatch {
wfProfileIn( __METHOD__ );
$this->file->lock();
-
+ // Leave private files alone
+ $privateFiles = array();
+ list( $oldRels, $deleteCurrent ) = $this->getOldRels();
+ $dbw = $this->file->repo->getMasterDB();
+ if( !empty( $oldRels ) ) {
+ $res = $dbw->select( 'oldimage',
+ array( 'oi_archive_name' ),
+ array( 'oi_name' => $this->file->getName(),
+ 'oi_archive_name IN (' . $dbw->makeList( array_keys($oldRels) ) . ')',
+ 'oi_deleted & ' . File::DELETED_FILE => File::DELETED_FILE ),
+ __METHOD__ );
+ while( $row = $dbw->fetchObject( $res ) ) {
+ $privateFiles[$row->oi_archive_name] = 1;
+ }
+ }
// Prepare deletion batch
$hashes = $this->getHashes();
$this->deletionBatch = array();
$ext = $this->file->getExtension();
$dotExt = $ext === '' ? '' : ".$ext";
foreach ( $this->srcRels as $name => $srcRel ) {
- // Skip files that have no hash (missing source)
- if ( isset( $hashes[$name] ) ) {
+ // Skip files that have no hash (missing source).
+ // Keep private files where they are.
+ if ( isset($hashes[$name]) && !array_key_exists($name,$privateFiles) ) {
$hash = $hashes[$name];
$key = $hash . $dotExt;
$dstRel = $this->file->repo->getDeletedHashPath( $key ) . $key;
@@ -1347,7 +1405,7 @@ class LocalFileDeleteBatch {
// Lock the filearchive rows so that the files don't get deleted by a cleanup operation
// We acquire this lock by running the inserts now, before the file operations.
//
- // This potentially has poor lock contention characteristics -- an alternative
+ // This potentially has poor lock contention characteristics -- an alternative
// scheme would be to insert stub filearchive entries with no fa_name and commit
// them in a separate transaction, then run the file ops, then update the fa_name fields.
$this->doDBInserts();
@@ -1390,14 +1448,16 @@ class LocalFileDeleteBatch {
/**
* Helper class for file undeletion
+ * @ingroup FileRepo
*/
class LocalFileRestoreBatch {
var $file, $cleanupBatch, $ids, $all, $unsuppress = false;
- function __construct( File $file ) {
+ function __construct( File $file, $unsuppress = false ) {
$this->file = $file;
$this->cleanupBatch = $this->ids = array();
$this->ids = array();
+ $this->unsuppress = $unsuppress;
}
/**
@@ -1420,9 +1480,9 @@ class LocalFileRestoreBatch {
function addAll() {
$this->all = true;
}
-
+
/**
- * Run the transaction, except the cleanup batch.
+ * Run the transaction, except the cleanup batch.
* The cleanup batch should be run in a separate transaction, because it locks different
* rows and there's no need to keep the image row locked while it's acquiring those locks
* The caller may have its own transaction open.
@@ -1438,7 +1498,7 @@ class LocalFileRestoreBatch {
$exists = $this->file->lock();
$dbw = $this->file->repo->getMasterDB();
$status = $this->file->repo->newGood();
-
+
// Fetch all or selected archived revisions for the file,
// sorted from the most recent to the oldest.
$conditions = array( 'fa_name' => $this->file->getName() );
@@ -1460,12 +1520,7 @@ class LocalFileRestoreBatch {
$archiveNames = array();
while( $row = $dbw->fetchObject( $result ) ) {
$idsPresent[] = $row->fa_id;
- if ( $this->unsuppress ) {
- // Currently, fa_deleted flags fall off upon restore, lets be careful about this
- } else if ( ($row->fa_deleted & Revision::DELETED_RESTRICTED) && !$wgUser->isAllowed('hiderevision') ) {
- // Skip restoring file revisions that the user cannot restore
- continue;
- }
+
if ( $row->fa_name != $this->file->getName() ) {
$status->error( 'undelete-filename-mismatch', $wgLang->timeanddate( $row->fa_timestamp ) );
$status->failCount++;
@@ -1486,7 +1541,7 @@ class LocalFileRestoreBatch {
if ( strlen( $sha1 ) == 32 && $sha1[0] == '0' ) {
$sha1 = substr( $sha1, 1 );
}
-
+
if( is_null( $row->fa_major_mime ) || $row->fa_major_mime == 'unknown'
|| is_null( $row->fa_minor_mime ) || $row->fa_minor_mime == 'unknown'
|| is_null( $row->fa_media_type ) || $row->fa_media_type == 'UNKNOWN'
@@ -1503,6 +1558,11 @@ class LocalFileRestoreBatch {
}
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(
@@ -1549,13 +1609,17 @@ class LocalFileRestoreBatch {
'oi_media_type' => $props['media_type'],
'oi_major_mime' => $props['major_mime'],
'oi_minor_mime' => $props['minor_mime'],
- 'oi_deleted' => $row->fa_deleted,
+ 'oi_deleted' => $this->unsuppress ? 0 : $row->fa_deleted,
'oi_sha1' => $sha1 );
}
$deleteIds[] = $row->fa_id;
- $storeBatch[] = array( $deletedUrl, 'public', $destRel );
- $this->cleanupBatch[] = $row->fa_storage_key;
+ if( !$this->unsuppress && $row->fa_deleted & File::DELETED_FILE ) {
+ // private files can stay where they are
+ } else {
+ $storeBatch[] = array( $deletedUrl, 'public', $destRel );
+ $this->cleanupBatch[] = $row->fa_storage_key;
+ }
$first = false;
}
unset( $result );
@@ -1580,8 +1644,8 @@ class LocalFileRestoreBatch {
// Run the DB updates
// Because we have locked the image row, key conflicts should be rare.
- // If they do occur, we can roll back the transaction at this time with
- // no data loss, but leaving unregistered files scattered throughout the
+ // If they do occur, we can roll back the transaction at this time with
+ // no data loss, but leaving unregistered files scattered throughout the
// public zone.
// This is not ideal, which is why it's important to lock the image row.
if ( $insertCurrent ) {
@@ -1591,8 +1655,8 @@ class LocalFileRestoreBatch {
$dbw->insert( 'oldimage', $insertBatch, __METHOD__ );
}
if ( $deleteIds ) {
- $dbw->delete( 'filearchive',
- array( 'fa_id IN (' . $dbw->makeList( $deleteIds ) . ')' ),
+ $dbw->delete( 'filearchive',
+ array( 'fa_id IN (' . $dbw->makeList( $deleteIds ) . ')' ),
__METHOD__ );
}
@@ -1627,3 +1691,146 @@ class LocalFileRestoreBatch {
return $status;
}
}
+
+#------------------------------------------------------------------------------
+
+/**
+ * Helper class for file movement
+ * @ingroup FileRepo
+ */
+class LocalFileMoveBatch {
+ var $file, $cur, $olds, $oldCount, $archive, $target, $db;
+
+ function __construct( File $file, Title $target ) {
+ $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->oldName = $this->file->getName();
+ $this->newName = $this->file->repo->getNameFromTitle( $this->target );
+ $this->oldRel = $this->oldHash . $this->oldName;
+ $this->newRel = $this->newHash . $this->newName;
+ $this->db = $file->repo->getMasterDb();
+ }
+
+ /*
+ * Add the current image to the batch
+ */
+ function addCurrent() {
+ $this->cur = array( $this->oldRel, $this->newRel );
+ }
+
+ /*
+ * Add the old versions of the image to the batch
+ */
+ function addOlds() {
+ $archiveBase = 'archive';
+ $this->olds = array();
+ $this->oldCount = 0;
+
+ $result = $this->db->select( 'oldimage',
+ array( 'oi_archive_name', 'oi_deleted' ),
+ array( 'oi_name' => $this->oldName ),
+ __METHOD__
+ );
+ while( $row = $this->db->fetchObject( $result ) ) {
+ $oldName = $row->oi_archive_name;
+ $bits = explode( '!', $oldName, 2 );
+ if( count( $bits ) != 2 ) {
+ wfDebug( 'Invalid old file name: ' . $oldName );
+ continue;
+ }
+ list( $timestamp, $filename ) = $bits;
+ if( $this->oldName != $filename ) {
+ wfDebug( 'Invalid old file name:' . $oldName );
+ continue;
+ }
+ $this->oldCount++;
+ // Do we want to add those to oldCount?
+ if( $row->oi_deleted & File::DELETED_FILE ) {
+ continue;
+ }
+ $this->olds[] = array(
+ "{$archiveBase}/{$this->oldHash}{$oldname}",
+ "{$archiveBase}/{$this->newHash}{$timestamp}!{$this->newName}"
+ );
+ }
+ $this->db->freeResult( $result );
+ }
+
+ /*
+ * Perform the move.
+ */
+ function execute() {
+ $repo = $this->file->repo;
+ $status = $repo->newGood();
+ $triplets = $this->getMoveTriplets();
+
+ $statusDb = $this->doDBUpdates();
+ wfDebugLog( 'imagemove', "Renamed {$this->file->name} in database: {$statusDb->successCount} successes, {$statusDb->failCount} failures" );
+ $statusMove = $repo->storeBatch( $triplets, FSRepo::DELETE_SOURCE );
+ wfDebugLog( 'imagemove', "Moved files for {$this->file->name}: {$statusMove->successCount} successes, {$statusMove->failCount} failures" );
+ if( !$statusMove->isOk() ) {
+ wfDebugLog( 'imagemove', "Error in moving files: " . $statusMove->getWikiText() );
+ $this->db->rollback();
+ }
+ $status->merge( $statusDb );
+ $status->merge( $statusMove );
+ return $status;
+ }
+
+ /*
+ * Do the database updates and return a new WikiError indicating how many
+ * rows where updated.
+ */
+ function doDBUpdates() {
+ $repo = $this->file->repo;
+ $status = $repo->newGood();
+ $dbw = $this->db;
+
+ // Update current image
+ $dbw->update(
+ 'image',
+ array( 'img_name' => $this->newName ),
+ array( 'img_name' => $this->oldName ),
+ __METHOD__
+ );
+ if( $dbw->affectedRows() ) {
+ $status->successCount++;
+ } else {
+ $status->failCount++;
+ }
+
+ // Update old images
+ $dbw->update(
+ 'oldimage',
+ array(
+ 'oi_name' => $this->newName,
+ 'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name', $dbw->addQuotes($this->oldName), $dbw->addQuotes($this->newName) ),
+ ),
+ array( 'oi_name' => $this->oldName ),
+ __METHOD__
+ );
+ $affected = $dbw->affectedRows();
+ $total = $this->oldCount;
+ $status->successCount += $affected;
+ $status->failCount += $total - $affected;
+
+ return $status;
+ }
+
+ /*
+ * Generate triplets for FSRepo::storeBatch().
+ */
+ function getMoveTriplets() {
+ $moves = array_merge( array( $this->cur ), $this->olds );
+ $triplets = array(); // The format is: (srcUrl, destZone, destUrl)
+ foreach( $moves as $move ) {
+ // $move: (oldRelativePath, newRelativePath)
+ $srcUrl = $this->file->repo->getVirtualUrl() . '/public/' . rawurlencode( $move[0] );
+ $triplets[] = array( $srcUrl, 'public', $move[1] );
+ wfDebugLog( 'imagemove', "Generated move triplet for {$this->file->name}: {$srcUrl} :: public :: {$move[1]}" );
+ }
+ return $triplets;
+ }
+}
diff --git a/includes/filerepo/LocalRepo.php b/includes/filerepo/LocalRepo.php
index a259bd48..90b198c8 100644
--- a/includes/filerepo/LocalRepo.php
+++ b/includes/filerepo/LocalRepo.php
@@ -2,10 +2,13 @@
/**
* A repository that stores files in the local filesystem and registers them
* in the wiki's own database. This is the most commonly used repository class.
+ * @ingroup FileRepo
*/
class LocalRepo extends FSRepo {
var $fileFactory = array( 'LocalFile', 'newFromTitle' );
var $oldFileFactory = array( 'OldLocalFile', 'newFromTitle' );
+ var $fileFromRowFactory = array( 'LocalFile', 'newFromRow' );
+ var $oldFileFromRowFactory = array( 'OldLocalFile', 'newFromRow' );
function getSlaveDB() {
return wfGetDB( DB_SLAVE );
@@ -15,24 +18,28 @@ class LocalRepo extends FSRepo {
return wfGetDB( DB_MASTER );
}
+ function getMemcKey( $key ) {
+ return wfWikiID( $this->getSlaveDB() ) . ":{$key}";
+ }
+
function newFileFromRow( $row ) {
if ( isset( $row->img_name ) ) {
- return LocalFile::newFromRow( $row, $this );
+ return call_user_func( $this->fileFromRowFactory, $row, $this );
} elseif ( isset( $row->oi_name ) ) {
- return OldLocalFile::newFromRow( $row, $this );
+ return call_user_func( $this->oldFileFromRowFactory, $row, $this );
} else {
throw new MWException( __METHOD__.': invalid row' );
}
}
-
+
function newFromArchiveName( $title, $archiveName ) {
return OldLocalFile::newFromArchiveName( $title, $this, $archiveName );
}
/**
- * Delete files in the deleted directory if they are not referenced in the
- * filearchive table. This needs to be done in the repo because it needs to
- * interleave database locks with file operations, which is potentially a
+ * Delete files in the deleted directory if they are not referenced in the
+ * filearchive table. This needs to be done in the repo because it needs to
+ * interleave database locks with file operations, which is potentially a
* remote operation.
* @return FileRepoStatus
*/
@@ -45,9 +52,19 @@ class LocalRepo extends FSRepo {
$hashPath = $this->getDeletedHashPath( $key );
$path = "$root/$hashPath$key";
$dbw->begin();
- $inuse = $dbw->selectField( 'filearchive', '1',
+ $inuse = $dbw->selectField( 'filearchive', '1',
array( 'fa_storage_group' => 'deleted', 'fa_storage_key' => $key ),
__METHOD__, array( 'FOR UPDATE' ) );
+ if( !$inuse ) {
+ $sha1 = substr( $key, 0, strcspn( $key, '.' ) );
+ $ext = substr( $key, strcspn($key,'.') + 1 );
+ $ext = File::normalizeExtension($ext);
+ $inuse = $dbw->selectField( 'oldimage', '1',
+ array( 'oi_sha1' => $sha1,
+ "oi_archive_name LIKE '%.{$ext}'",
+ 'oi_deleted & '.File::DELETED_FILE => File::DELETED_FILE ),
+ __METHOD__, array( 'FOR UPDATE' ) );
+ }
if ( !$inuse ) {
wfDebug( __METHOD__ . ": deleting $key\n" );
if ( !@unlink( $path ) ) {
@@ -67,7 +84,7 @@ class LocalRepo extends FSRepo {
* Function link Title::getArticleID().
* We can't say Title object, what database it should use, so we duplicate that function here.
*/
- private function getArticleID( $title ) {
+ protected function getArticleID( $title ) {
if( !$title instanceof Title ) {
return 0;
}
@@ -85,17 +102,26 @@ class LocalRepo extends FSRepo {
}
function checkRedirect( $title ) {
- global $wgFileRedirects;
- if( !$wgFileRedirects ) {
- return false;
- }
+ global $wgMemc;
+ if( is_string( $title ) ) {
+ $title = Title::newFromTitle( $title );
+ }
if( $title instanceof Title && $title->getNamespace() == NS_MEDIA ) {
$title = Title::makeTitle( NS_IMAGE, $title->getText() );
}
-
+
+ $memcKey = $this->getMemcKey( "image_redirect:" . md5( $title->getPrefixedDBkey() ) );
+ $cachedValue = $wgMemc->get( $memcKey );
+ if( $cachedValue ) {
+ return Title::newFromDbKey( $cachedValue );
+ } elseif( $cachedValue == ' ' ) { # FIXME: ugly hack, but BagOStuff caching seems to be weird and return false if !cachedValue, not only if it doesn't exist
+ return false;
+ }
+
$id = $this->getArticleID( $title );
if( !$id ) {
+ $wgMemc->set( $memcKey, " ", 9000 );
return false;
}
$dbr = $this->getSlaveDB();
@@ -105,9 +131,58 @@ class LocalRepo extends FSRepo {
array( 'rd_from' => $id ),
__METHOD__
);
+
+ if( $row ) $targetTitle = Title::makeTitle( $row->rd_namespace, $row->rd_title );
+ $wgMemc->set( $memcKey, ($row ? $targetTitle->getPrefixedDBkey() : " "), 9000 );
if( !$row ) {
return false;
}
- return Title::makeTitle( $row->rd_namespace, $row->rd_title );
+ return $targetTitle;
+ }
+
+ function invalidateImageRedirect( $title ) {
+ global $wgMemc;
+ $memcKey = $this->getMemcKey( "image_redirect:" . md5( $title->getPrefixedDBkey() ) );
+ $wgMemc->delete( $memcKey );
+ }
+
+ function findBySha1( $hash ) {
+ $dbr = $this->getSlaveDB();
+ $res = $dbr->select(
+ 'image',
+ LocalFile::selectFields(),
+ array( 'img_sha1' => $hash )
+ );
+
+ $result = array();
+ while ( $row = $res->fetchObject() )
+ $result[] = $this->newFileFromRow( $row );
+ $res->free();
+ return $result;
+ }
+
+ /*
+ * Find many files using one query
+ */
+ function findFiles( $titles, $flags ) {
+ // FIXME: Comply with $flags
+ // FIXME: Only accepts a $titles array where the keys are the sanitized
+ // file names.
+
+ if ( count( $titles ) == 0 ) return array();
+
+ $dbr = $this->getSlaveDB();
+ $res = $dbr->select(
+ 'image',
+ LocalFile::selectFields(),
+ array( 'img_name' => array_keys( $titles ) )
+ );
+
+ $result = array();
+ while ( $row = $res->fetchObject() ) {
+ $result[$row->img_name] = $this->newFileFromRow( $row );
+ }
+ $res->free();
+ return $result;
}
}
diff --git a/includes/filerepo/NullRepo.php b/includes/filerepo/NullRepo.php
index 87bfd3ab..fb89cebb 100644
--- a/includes/filerepo/NullRepo.php
+++ b/includes/filerepo/NullRepo.php
@@ -2,15 +2,15 @@
/**
* File repository with no files, for performance testing
+ * @ingroup FileRepo
*/
-
class NullRepo extends FileRepo {
function __construct( $info ) {}
-
+
function storeBatch( $triplets, $flags = 0 ) {
return false;
}
-
+
function storeTemp( $originalName, $srcPath ) {
return false;
}
@@ -30,5 +30,3 @@ class NullRepo extends FileRepo {
return false;
}
}
-
-?>
diff --git a/includes/filerepo/OldLocalFile.php b/includes/filerepo/OldLocalFile.php
index 850a8d8a..89e49c4c 100644
--- a/includes/filerepo/OldLocalFile.php
+++ b/includes/filerepo/OldLocalFile.php
@@ -3,7 +3,7 @@
/**
* Class to represent a file in the oldimage table
*
- * @addtogroup FileRepo
+ * @ingroup FileRepo
*/
class OldLocalFile extends LocalFile {
var $requestedTime, $archive_name;
@@ -11,7 +11,10 @@ class OldLocalFile extends LocalFile {
const CACHE_VERSION = 1;
const MAX_CACHE_ROWS = 20;
- static function newFromTitle( $title, $repo, $time ) {
+ static function newFromTitle( $title, $repo, $time = null ) {
+ # The null default value is only here to avoid an E_STRICT
+ if( $time === null )
+ throw new MWException( __METHOD__.' got null for $time parameter' );
return new self( $title, $repo, $time, null );
}
@@ -25,6 +28,45 @@ class OldLocalFile extends LocalFile {
$file->loadFromRow( $row, 'oi_' );
return $file;
}
+
+ static function newFromKey( $sha1, $repo, $timestamp = false ) {
+ # Polymorphic function name to distinguish foreign and local fetches
+ $fname = get_class( $this ) . '::' . __FUNCTION__;
+
+ $conds = array( 'oi_sha1' => $sha1 );
+ if( $timestamp ) {
+ $conds['oi_timestamp'] = $timestamp;
+ }
+ $row = $dbr->selectRow( 'oldimage', $this->getCacheFields( 'oi_' ), $conds, $fname );
+ if( $row ) {
+ return self::newFromRow( $row, $repo );
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Fields in the oldimage table
+ */
+ static function selectFields() {
+ return array(
+ 'oi_name',
+ 'oi_archive_name',
+ 'oi_size',
+ 'oi_width',
+ 'oi_height',
+ 'oi_metadata',
+ 'oi_bits',
+ 'oi_media_type',
+ 'oi_major_mime',
+ 'oi_minor_mime',
+ 'oi_description',
+ 'oi_user',
+ 'oi_user_text',
+ 'oi_timestamp',
+ 'oi_sha1',
+ );
+ }
/**
* @param Title $title
@@ -42,8 +84,7 @@ class OldLocalFile extends LocalFile {
}
function getCacheKey() {
- $hashedName = md5($this->getName());
- return wfMemcKey( 'oldfile', $hashedName );
+ return false;
}
function getArchiveName() {
@@ -57,103 +98,8 @@ class OldLocalFile extends LocalFile {
return true;
}
- /**
- * Try to load file metadata from memcached. Returns true on success.
- */
- function loadFromCache() {
- global $wgMemc;
- wfProfileIn( __METHOD__ );
- $this->dataLoaded = false;
- $key = $this->getCacheKey();
- if ( !$key ) {
- return false;
- }
- $oldImages = $wgMemc->get( $key );
-
- if ( isset( $oldImages['version'] ) && $oldImages['version'] == self::CACHE_VERSION ) {
- unset( $oldImages['version'] );
- $more = isset( $oldImages['more'] );
- unset( $oldImages['more'] );
- $found = false;
- if ( is_null( $this->requestedTime ) ) {
- foreach ( $oldImages as $timestamp => $info ) {
- if ( $info['archive_name'] == $this->archive_name ) {
- $found = true;
- break;
- }
- }
- } else {
- krsort( $oldImages );
- foreach ( $oldImages as $timestamp => $info ) {
- if ( $timestamp <= $this->requestedTime ) {
- $found = true;
- break;
- }
- }
- }
- if ( $found ) {
- wfDebug( "Pulling file metadata from cache key {$key}[{$timestamp}]\n" );
- $this->dataLoaded = true;
- $this->fileExists = true;
- foreach ( $info as $name => $value ) {
- $this->$name = $value;
- }
- } elseif ( $more ) {
- wfDebug( "Cache key was truncated, oldimage row might be found in the database\n" );
- } else {
- wfDebug( "Image did not exist at the specified time.\n" );
- $this->fileExists = false;
- $this->dataLoaded = true;
- }
- }
-
- if ( $this->dataLoaded ) {
- wfIncrStats( 'image_cache_hit' );
- } else {
- wfIncrStats( 'image_cache_miss' );
- }
-
- wfProfileOut( __METHOD__ );
- return $this->dataLoaded;
- }
-
- function saveToCache() {
- // If a timestamp was specified, cache the entire history of the image (up to MAX_CACHE_ROWS).
- if ( is_null( $this->requestedTime ) ) {
- return;
- }
- // This is expensive, so we only do it if $wgMemc is real
- global $wgMemc;
- if ( $wgMemc instanceof FakeMemcachedClient ) {
- return;
- }
- $key = $this->getCacheKey();
- if ( !$key ) {
- return;
- }
- wfProfileIn( __METHOD__ );
-
- $dbr = $this->repo->getSlaveDB();
- $res = $dbr->select( 'oldimage', $this->getCacheFields( 'oi_' ),
- array( 'oi_name' => $this->getName() ), __METHOD__,
- array(
- 'LIMIT' => self::MAX_CACHE_ROWS + 1,
- 'ORDER BY' => 'oi_timestamp DESC',
- ));
- $cache = array( 'version' => self::CACHE_VERSION );
- $numRows = $dbr->numRows( $res );
- if ( $numRows > self::MAX_CACHE_ROWS ) {
- $cache['more'] = true;
- $numRows--;
- }
- for ( $i = 0; $i < $numRows; $i++ ) {
- $row = $dbr->fetchObject( $res );
- $decoded = $this->decodeRow( $row, 'oi_' );
- $cache[$row->oi_timestamp] = $decoded;
- }
- $dbr->freeResult( $res );
- $wgMemc->set( $key, $cache, 7*86400 /* 1 week */ );
- wfProfileOut( __METHOD__ );
+ function isVisible() {
+ return $this->exists() && !$this->isDeleted(File::DELETED_FILE);
}
function loadFromDB() {
@@ -164,7 +110,7 @@ class OldLocalFile extends LocalFile {
if ( is_null( $this->requestedTime ) ) {
$conds['oi_archive_name'] = $this->archive_name;
} else {
- $conds[] = 'oi_timestamp <= ' . $dbr->addQuotes( $this->requestedTime );
+ $conds[] = 'oi_timestamp = ' . $dbr->addQuotes( $dbr->timestamp( $this->requestedTime ) );
}
$row = $dbr->selectRow( 'oldimage', $this->getCacheFields( 'oi_' ),
$conds, __METHOD__, array( 'ORDER BY' => 'oi_timestamp DESC' ) );
@@ -179,10 +125,7 @@ class OldLocalFile extends LocalFile {
function getCacheFields( $prefix = 'img_' ) {
$fields = parent::getCacheFields( $prefix );
$fields[] = $prefix . 'archive_name';
-
- // XXX: Temporary hack before schema update
- //$fields = array_diff( $fields, array(
- // 'oi_media_type', 'oi_major_mime', 'oi_minor_mime', 'oi_metadata' ) );
+ $fields[] = $prefix . 'deleted';
return $fields;
}
@@ -193,11 +136,11 @@ class OldLocalFile extends LocalFile {
function getUrlRel() {
return 'archive/' . $this->getHashPath() . urlencode( $this->getArchiveName() );
}
-
+
function upgradeRow() {
wfProfileIn( __METHOD__ );
$this->loadFromFile();
-
+
# Don't destroy file info of missing files
if ( !$this->fileExists ) {
wfDebug( __METHOD__.": file does not exist, aborting\n" );
@@ -219,14 +162,39 @@ class OldLocalFile extends LocalFile {
'oi_minor_mime' => $minor,
'oi_metadata' => $this->metadata,
'oi_sha1' => $this->sha1,
- ), array(
- 'oi_name' => $this->getName(),
+ ), array(
+ 'oi_name' => $this->getName(),
'oi_archive_name' => $this->archive_name ),
__METHOD__
);
wfProfileOut( __METHOD__ );
}
-}
-
+ /**
+ * int $field one of DELETED_* bitfield constants
+ * for file or revision rows
+ * @return bool
+ */
+ function isDeleted( $field ) {
+ return ($this->deleted & $field) == $field;
+ }
+ /**
+ * Determine if the current user is allowed to view a particular
+ * field of this FileStore image file, if it's marked as deleted.
+ * @param int $field
+ * @return bool
+ */
+ function userCan( $field ) {
+ if( isset($this->deleted) && ($this->deleted & $field) == $field ) {
+ global $wgUser;
+ $permission = ( $this->deleted & File::DELETED_RESTRICTED ) == File::DELETED_RESTRICTED
+ ? 'suppressrevision'
+ : 'deleterevision';
+ wfDebug( "Checking for $permission due to $field match on $this->mDeleted\n" );
+ return $wgUser->isAllowed( $permission );
+ } else {
+ return true;
+ }
+ }
+}
diff --git a/includes/filerepo/README b/includes/filerepo/README
index 03cb8b3b..d3aea9f0 100644
--- a/includes/filerepo/README
+++ b/includes/filerepo/README
@@ -1,8 +1,8 @@
Some quick notes on the file/repository architecture.
-Functionality is, as always, driven by data model.
+Functionality is, as always, driven by data model.
-* The repository object stores configuration information about a file storage
+* The repository object stores configuration information about a file storage
method.
* The file object is a process-local cache of information about a particular
@@ -28,14 +28,14 @@ even entire classes, between repositories.
These rules alone still do lead to some ambiguity -- it may not be clear whether
to implement some functionality in a repository function with a filename
-parameter, or in the file object itself.
+parameter, or in the file object itself.
-So we introduce the following rule: the file subclass is smarter than the
+So we introduce the following rule: the file subclass is smarter than the
repository subclass. The repository should in general provide a minimal API
-needed to access the storage backend efficiently.
+needed to access the storage backend efficiently.
-In particular, note that I have not implemented any database access in
-LocalRepo.php. LocalRepo provides only file access, and LocalFile provides
+In particular, note that I have not implemented any database access in
+LocalRepo.php. LocalRepo provides only file access, and LocalFile provides
database access and higher-level functions such as cache management.
Tim Starling, June 2007
diff --git a/includes/filerepo/RepoGroup.php b/includes/filerepo/RepoGroup.php
index b0e1d782..7cb837b3 100644
--- a/includes/filerepo/RepoGroup.php
+++ b/includes/filerepo/RepoGroup.php
@@ -1,8 +1,14 @@
<?php
+/**
+ * @defgroup FileRepo FileRepo
+ *
+ * @file
+ * @ingroup FileRepo
+ */
/**
+ * @ingroup FileRepo
* Prioritized list of file repositories
- * @addtogroup filerepo
*/
class RepoGroup {
var $localRepo, $foreignRepos, $reposInitialised = false;
@@ -39,9 +45,9 @@ class RepoGroup {
}
/**
- * Construct a group of file repositories.
- * @param array $data Array of repository info arrays.
- * Each info array is an associative array with the 'class' member
+ * Construct a group of file repositories.
+ * @param array $data Array of repository info arrays.
+ * Each info array is an associative array with the 'class' member
* giving the class name. The entire array is passed to the repository
* constructor as the first parameter.
*/
@@ -54,27 +60,52 @@ class RepoGroup {
* Search repositories for an image.
* You can also use wfGetFile() to do this.
* @param mixed $title Title object or string
- * @param mixed $time The 14-char timestamp the file should have
+ * @param mixed $time The 14-char timestamp the file should have
* been uploaded, or false for the current version
+ * @param mixed $flags FileRepo::FIND_ flags
* @return File object or false if it is not found
*/
- function findFile( $title, $time = false ) {
+ function findFile( $title, $time = false, $flags = 0 ) {
if ( !$this->reposInitialised ) {
$this->initialiseRepos();
}
- $image = $this->localRepo->findFile( $title, $time );
+ $image = $this->localRepo->findFile( $title, $time, $flags );
if ( $image ) {
return $image;
}
foreach ( $this->foreignRepos as $repo ) {
- $image = $repo->findFile( $title, $time );
+ $image = $repo->findFile( $title, $time, $flags );
if ( $image ) {
return $image;
}
}
return false;
}
+ function findFiles( $titles, $flags = 0 ) {
+ if ( !$this->reposInitialised ) {
+ $this->initialiseRepos();
+ }
+
+ $titleObjs = array();
+ foreach ( $titles as $title ) {
+ if ( !( $title instanceof Title ) )
+ $title = Title::makeTitleSafe( NS_IMAGE, $title );
+ $titleObjs[$title->getDBkey()] = $title;
+ }
+
+ $images = $this->localRepo->findFiles( $titleObjs, $flags );
+
+ foreach ( $this->foreignRepos as $repo ) {
+ // Remove found files from $titleObjs
+ foreach ( $images as $name => $image )
+ if ( isset( $titleObjs[$name] ) )
+ unset( $titleObjs[$name] );
+
+ $images = array_merge( $images, $repo->findFiles( $titleObjs, $flags ) );
+ }
+ return $images;
+ }
/**
* Interface for FileRepo::checkRedirect()
@@ -96,6 +127,17 @@ class RepoGroup {
}
return false;
}
+
+ function findBySha1( $hash ) {
+ if ( !$this->reposInitialised ) {
+ $this->initialiseRepos();
+ }
+
+ $result = $this->localRepo->findBySha1( $hash );
+ foreach ( $this->foreignRepos as $repo )
+ $result = array_merge( $result, $repo->findBySha1( $hash ) );
+ return $result;
+ }
/**
* Get the repo instance with a given key.
@@ -134,6 +176,20 @@ class RepoGroup {
return $this->getRepo( 'local' );
}
+ function forEachForeignRepo( $callback, $params = array() ) {
+ foreach( $this->foreignRepos as $repo ) {
+ $args = array_merge( array( $repo ), $params );
+ if( call_user_func_array( $callback, $args ) ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ function hasForeignRepos() {
+ return !empty( $this->foreignRepos );
+ }
+
/**
* Initialise the $repos array
*/
@@ -187,5 +243,3 @@ class RepoGroup {
}
}
}
-
-
diff --git a/includes/filerepo/UnregisteredLocalFile.php b/includes/filerepo/UnregisteredLocalFile.php
index 419c61f6..c687ef6e 100644
--- a/includes/filerepo/UnregisteredLocalFile.php
+++ b/includes/filerepo/UnregisteredLocalFile.php
@@ -1,14 +1,16 @@
<?php
/**
- * A file object referring to either a standalone local file, or a file in a
+ * A file object referring to either a standalone local file, or a file in a
* local repository with no database, for example an FSRepo repository.
*
* Read-only.
*
- * TODO: Currently it doesn't really work in the repository role, there are
- * lots of functions missing. It is used by the WebStore extension in the
+ * TODO: Currently it doesn't really work in the repository role, there are
+ * lots of functions missing. It is used by the WebStore extension in the
* standalone role.
+ *
+ * @ingroup FileRepo
*/
class UnregisteredLocalFile extends File {
var $title, $path, $mime, $handler, $dims;
@@ -106,4 +108,3 @@ class UnregisteredLocalFile extends File {
}
}
}
-
diff --git a/includes/media/BMP.php b/includes/media/BMP.php
index 2f451b0a..ce1b0362 100644
--- a/includes/media/BMP.php
+++ b/includes/media/BMP.php
@@ -1,10 +1,14 @@
<?php
+/**
+ * @file
+ * @ingroup Media
+ */
/**
* Handler for Microsoft's bitmap format; getimagesize() doesn't
* support these files
*
- * @addtogroup Media
+ * @ingroup Media
*/
class BmpHandler extends BitmapHandler {
@@ -26,4 +30,4 @@ class BmpHandler extends BitmapHandler {
$h = unpack( 'V' , $h );
return array( $w[1], $h[1] );
}
-} \ No newline at end of file
+}
diff --git a/includes/media/Bitmap.php b/includes/media/Bitmap.php
index ca82aab0..e01386e9 100644
--- a/includes/media/Bitmap.php
+++ b/includes/media/Bitmap.php
@@ -1,7 +1,11 @@
<?php
+/**
+ * @file
+ * @ingroup Media
+ */
/**
- * @addtogroup Media
+ * @ingroup Media
*/
class BitmapHandler extends ImageHandler {
function normaliseParams( $image, &$params ) {
@@ -26,7 +30,7 @@ class BitmapHandler extends ImageHandler {
# Don't make an image bigger than the source
$params['physicalWidth'] = $params['width'];
$params['physicalHeight'] = $params['height'];
-
+
if ( $params['physicalWidth'] >= $srcWidth ) {
$params['physicalWidth'] = $srcWidth;
$params['physicalHeight'] = $srcHeight;
@@ -35,7 +39,7 @@ class BitmapHandler extends ImageHandler {
return true;
}
-
+
function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
global $wgUseImageMagick, $wgImageMagickConvertCommand;
global $wgCustomConvertCommand;
@@ -85,8 +89,8 @@ class BitmapHandler extends ImageHandler {
}
if ( !wfMkdirParents( dirname( $dstPath ) ) ) {
- return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight,
- wfMsg( 'thumbnail_dest_directory' ) );
+ wfDebug( "Unable to create thumbnail destination directory, falling back to client scaling\n" );
+ return new ThumbnailImage( $image, $image->getURL(), $clientWidth, $clientHeight, $srcPath );
}
if ( $scaler == 'im' ) {
@@ -167,12 +171,12 @@ class BitmapHandler extends ImageHandler {
$src_image = call_user_func( $loader, $srcPath );
$dst_image = imagecreatetruecolor( $physicalWidth, $physicalHeight );
-
+
// Initialise the destination image to transparent instead of
// the default solid black, to support PNG and GIF transparency nicely
$background = imagecolorallocate( $dst_image, 0, 0, 0 );
imagecolortransparent( $dst_image, $background );
- imagealphablending( $dst_image, false );
+ imagealphablending( $dst_image, false );
if( $colorStyle == 'palette' ) {
// Don't resample for paletted GIF images.
@@ -187,7 +191,7 @@ class BitmapHandler extends ImageHandler {
}
imagesavealpha( $dst_image, true );
-
+
call_user_func( $saveType, $dst_image, $dstPath );
imagedestroy( $dst_image );
imagedestroy( $src_image );
@@ -303,5 +307,3 @@ class BitmapHandler extends ImageHandler {
return $result;
}
}
-
-
diff --git a/includes/media/DjVu.php b/includes/media/DjVu.php
index 20e59d18..f0bbcc51 100644
--- a/includes/media/DjVu.php
+++ b/includes/media/DjVu.php
@@ -1,7 +1,11 @@
<?php
-
/**
- * @addtogroup Media
+ * @file
+ * @ingroup Media
+ */
+
+/**
+ * @ingroup Media
*/
class DjVuHandler extends ImageHandler {
function isEnabled() {
@@ -63,14 +67,14 @@ class DjVuHandler extends ImageHandler {
function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
global $wgDjvuRenderer, $wgDjvuPostProcessor;
- // Fetch XML and check it, to give a more informative error message than the one which
+ // Fetch XML and check it, to give a more informative error message than the one which
// normaliseParams will inevitably give.
$xml = $image->getMetadata();
if ( !$xml ) {
- return new MediaTransformError( 'thumbnail_error', @$params['width'], @$params['height'],
+ return new MediaTransformError( 'thumbnail_error', @$params['width'], @$params['height'],
wfMsg( 'djvu_no_xml' ) );
}
-
+
if ( !$this->normaliseParams( $image, $params ) ) {
return new TransformParameterError( $params );
}
@@ -81,7 +85,7 @@ class DjVuHandler extends ImageHandler {
if ( $page > $this->pageCount( $image ) ) {
return new MediaTransformError( 'thumbnail_error', $width, $height, wfMsg( 'djvu_page_error' ) );
}
-
+
if ( $flags & self::TRANSFORM_LATER ) {
return new ThumbnailImage( $image, $dstUrl, $width, $height, $dstPath, $page );
}
@@ -90,7 +94,7 @@ class DjVuHandler extends ImageHandler {
return new MediaTransformError( 'thumbnail_error', $width, $height, wfMsg( 'thumbnail_dest_directory' ) );
}
- # Use a subshell (brackets) to aggregate stderr from both pipeline commands
+ # Use a subshell (brackets) to aggregate stderr from both pipeline commands
# before redirecting it to the overall stdout. This works in both Linux and Windows XP.
$cmd = '(' . wfEscapeShellArg( $wgDjvuRenderer ) . " -format=ppm -page={$page} -size={$width}x{$height} " .
wfEscapeShellArg( $srcPath );
@@ -208,5 +212,3 @@ class DjVuHandler extends ImageHandler {
}
}
}
-
-
diff --git a/includes/media/Generic.php b/includes/media/Generic.php
index 19914929..b2cb70f6 100644
--- a/includes/media/Generic.php
+++ b/includes/media/Generic.php
@@ -1,13 +1,14 @@
<?php
-
/**
* Media-handling base classes and generic functionality
+ * @file
+ * @ingroup Media
*/
/**
* Base media handler class
*
- * @addtogroup Media
+ * @ingroup Media
*/
abstract class MediaHandler {
const TRANSFORM_LATER = 1;
@@ -43,7 +44,7 @@ abstract class MediaHandler {
abstract function getParamMap();
/*
- * Validate a thumbnail parameter at parse time.
+ * Validate a thumbnail parameter at parse time.
* Return true to accept the parameter, and false to reject it.
* If you return false, the parser will do something quiet and forgiving.
*/
@@ -60,14 +61,14 @@ abstract class MediaHandler {
abstract function parseParamString( $str );
/**
- * Changes the parameter array as necessary, ready for transformation.
+ * Changes the parameter array as necessary, ready for transformation.
* Should be idempotent.
* Returns false if the parameters are unacceptable and the transform should fail
*/
abstract function normaliseParams( $image, &$params );
/**
- * Get an image size array like that returned by getimagesize(), or false if it
+ * Get an image size array like that returned by getimagesize(), or false if it
* can't be determined.
*
* @param Image $image The image object, or false if there isn't one
@@ -110,7 +111,7 @@ abstract class MediaHandler {
}
/**
- * Get a MediaTransformOutput object representing the transformed output. Does not
+ * Get a MediaTransformOutput object representing the transformed output. Does not
* actually do the transform.
*
* @param Image $image The image object
@@ -123,7 +124,7 @@ abstract class MediaHandler {
}
/**
- * Get a MediaTransformOutput object representing the transformed output. Does the
+ * Get a MediaTransformOutput object representing the transformed output. Does the
* transform unless $flags contains self::TRANSFORM_LATER.
*
* @param Image $image The image object
@@ -140,14 +141,14 @@ abstract class MediaHandler {
*/
function getThumbType( $ext, $mime ) {
return array( $ext, $mime );
- }
+ }
/**
* True if the handled types can be transformed
*/
function canRender( $file ) { return true; }
/**
- * True if handled types cannot be displayed directly in a browser
+ * True if handled types cannot be displayed directly in a browser
* but can be rendered
*/
function mustRender( $file ) { return false; }
@@ -166,7 +167,7 @@ abstract class MediaHandler {
/**
* Get an associative array of page dimensions
- * Currently "width" and "height" are understood, but this might be
+ * Currently "width" and "height" are understood, but this might be
* expanded in the future.
* Returns false if unknown or if the document is not multi-page.
*/
@@ -191,7 +192,7 @@ abstract class MediaHandler {
* ...
* )
* )
- * The UI will format this into a table where the visible fields are always
+ * The UI will format this into a table where the visible fields are always
* visible, and the collapsed fields are optionally visible.
*
* The function should return false if there is no metadata to display.
@@ -199,7 +200,7 @@ abstract class MediaHandler {
/**
* FIXME: I don't really like this interface, it's not very flexible
- * I think the media handler should generate HTML instead. It can do
+ * I think the media handler should generate HTML instead. It can do
* all the formatting according to some standard. That makes it possible
* to do things like visual indication of grouped and chained streams
* in ogg container files.
@@ -234,7 +235,9 @@ abstract class MediaHandler {
function getLongDesc( $file ) {
global $wgUser;
$sk = $wgUser->getSkin();
- return wfMsg( 'file-info', $sk->formatSize( $file->getSize() ), $file->getMimeType() );
+ return wfMsgExt( 'file-info', 'parseinline',
+ $sk->formatSize( $file->getSize() ),
+ $file->getMimeType() );
}
function getDimensionsString( $file ) {
@@ -272,7 +275,7 @@ abstract class MediaHandler {
/**
* Media handler abstract base class for images
*
- * @addtogroup Media
+ * @ingroup Media
*/
abstract class ImageHandler extends MediaHandler {
function canRender( $file ) {
@@ -354,13 +357,13 @@ abstract class ImageHandler extends MediaHandler {
function getTransform( $image, $dstPath, $dstUrl, $params ) {
return $this->doTransform( $image, $dstPath, $dstUrl, $params, self::TRANSFORM_LATER );
}
-
+
/**
* Validate thumbnail parameters and fill in the correct height
*
* @param integer &$width Specified width (input/output)
* @param integer &$height Height (output only)
- * @return false to indicate that an error should be returned to the user.
+ * @return false to indicate that an error should be returned to the user.
*/
function validateThumbParams( &$width, &$height, $srcWidth, $srcHeight, $mimeType ) {
$width = intval( $width );
@@ -385,7 +388,7 @@ abstract class ImageHandler extends MediaHandler {
}
$url = $script . '&' . wfArrayToCGI( $this->getScriptParams( $params ) );
$page = isset( $params['page'] ) ? $params['page'] : false;
-
+
if( $image->mustRender() || $params['width'] < $image->getWidth() ) {
return new ThumbnailImage( $image, $url, $params['width'], $params['height'], $page );
}
@@ -400,8 +403,8 @@ abstract class ImageHandler extends MediaHandler {
function getShortDesc( $file ) {
global $wgLang;
- $nbytes = '(' . wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ),
- $wgLang->formatNum( $file->getSize() ) ) . ')';
+ $nbytes = wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ),
+ $wgLang->formatNum( $file->getSize() ) );
$widthheight = wfMsgHtml( 'widthheight', $wgLang->formatNum( $file->getWidth() ) ,$wgLang->formatNum( $file->getHeight() ) );
return "$widthheight ($nbytes)";
@@ -409,17 +412,24 @@ abstract class ImageHandler extends MediaHandler {
function getLongDesc( $file ) {
global $wgLang;
- return wfMsgHtml('file-info-size', $wgLang->formatNum( $file->getWidth() ), $wgLang->formatNum( $file->getHeight() ),
- $wgLang->formatSize( $file->getSize() ), $file->getMimeType() );
+ return wfMsgExt('file-info-size', 'parseinline',
+ $wgLang->formatNum( $file->getWidth() ),
+ $wgLang->formatNum( $file->getHeight() ),
+ $wgLang->formatSize( $file->getSize() ),
+ $file->getMimeType() );
}
function getDimensionsString( $file ) {
global $wgLang;
$pages = $file->pageCount();
+ $width = $wgLang->formatNum( $file->getWidth() );
+ $height = $wgLang->formatNum( $file->getHeight() );
+ $pagesFmt = $wgLang->formatNum( $pages );
+
if ( $pages > 1 ) {
- return wfMsg( 'widthheightpage', $wgLang->formatNum( $file->getWidth() ), $wgLang->formatNum( $file->getHeight() ), $wgLang->formatNum( $pages ) );
+ return wfMsgExt( 'widthheightpage', 'parsemag', $width, $height, $pagesFmt );
} else {
- return wfMsg( 'widthheight', $wgLang->formatNum( $file->getWidth() ), $wgLang->formatNum( $file->getHeight() ) );
+ return wfMsg( 'widthheight', $width, $height );
}
}
}
diff --git a/includes/media/SVG.php b/includes/media/SVG.php
index 75d0ad3d..2604e3b4 100644
--- a/includes/media/SVG.php
+++ b/includes/media/SVG.php
@@ -1,7 +1,11 @@
<?php
+/**
+ * @file
+ * @ingroup Media
+ */
/**
- * @addtogroup Media
+ * @ingroup Media
*/
class SvgHandler extends ImageHandler {
function isEnabled() {
@@ -35,10 +39,10 @@ class SvgHandler extends ImageHandler {
}
return true;
}
-
+
function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
global $wgSVGConverters, $wgSVGConverter, $wgSVGConverterPath;
-
+
if ( !$this->normaliseParams( $image, $params ) ) {
return new TransformParameterError( $params );
}
@@ -53,7 +57,7 @@ class SvgHandler extends ImageHandler {
}
if ( !wfMkdirParents( dirname( $dstPath ) ) ) {
- return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight,
+ return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight,
wfMsg( 'thumbnail_dest_directory' ) );
}
@@ -94,9 +98,9 @@ class SvgHandler extends ImageHandler {
function getLongDesc( $file ) {
global $wgLang;
- return wfMsg( 'svg-long-desc', $file->getWidth(), $file->getHeight(),
+ return wfMsgExt( 'svg-long-desc', 'parseinline',
+ $wgLang->formatNum( $file->getWidth() ),
+ $wgLang->formatNum( $file->getHeight() ),
$wgLang->formatSize( $file->getSize() ) );
}
}
-
-
diff --git a/includes/memcached-client.php b/includes/memcached-client.php
index 2eddb908..6bd18387 100644
--- a/includes/memcached-client.php
+++ b/includes/memcached-client.php
@@ -57,47 +57,47 @@
* $val = $mc->get('key');
*
* @author Ryan T. Dean <rtdean@cytherianage.net>
- * @package memcached-client
* @version 0.1.2
*/
// {{{ requirements
// }}}
-// {{{ constants
-// {{{ flags
-
-/**
- * Flag: indicates data is serialized
- */
-define("MEMCACHE_SERIALIZED", 1<<0);
-
-/**
- * Flag: indicates data is compressed
- */
-define("MEMCACHE_COMPRESSED", 1<<1);
-
-// }}}
-
-/**
- * Minimum savings to store data compressed
- */
-define("COMPRESSION_SAVINGS", 0.20);
-
-// }}}
-
// {{{ class memcached
/**
* memcached client class implemented using (p)fsockopen()
*
* @author Ryan T. Dean <rtdean@cytherianage.net>
- * @addtogroup Cache
+ * @ingroup Cache
*/
class memcached
{
// {{{ properties
// {{{ public
+ // {{{ constants
+ // {{{ flags
+
+ /**
+ * Flag: indicates data is serialized
+ */
+ const SERIALIZED = 1;
+
+ /**
+ * Flag: indicates data is compressed
+ */
+ const COMPRESSED = 2;
+
+ // }}}
+
+ /**
+ * Minimum savings to store data compressed
+ */
+ const COMPRESSION_SAVINGS = 0.20;
+
+ // }}}
+
+
/**
* Command statistics
*
@@ -152,7 +152,7 @@ class memcached
/**
* At how many bytes should we compress?
*
- * @var integer
+ * @var integer
* @access private
*/
var $_compress_threshold;
@@ -192,7 +192,7 @@ class memcached
/**
* Total # of bit buckets we have
*
- * @var integer
+ * @var integer
* @access private
*/
var $_bucketcount;
@@ -200,7 +200,7 @@ class memcached
/**
* # of total servers we have
*
- * @var integer
+ * @var integer
* @access private
*/
var $_active;
@@ -401,6 +401,10 @@ class memcached
$fname = 'memcached::get';
wfProfileIn( $fname );
+ if ( $this->_debug ) {
+ $this->_debugprint( "get($key)\n" );
+ }
+
if (!$this->_active) {
wfProfileOut( $fname );
return false;
@@ -428,7 +432,7 @@ class memcached
if ($this->_debug)
foreach ($val as $k => $v)
- $this->_debugprint(@sprintf("MemCache: sock %s got %s => %s\r\n", serialize($sock), $k, $v));
+ $this->_debugprint(sprintf("MemCache: sock %s got %s\n", serialize($sock), $k));
wfProfileOut( $fname );
return @$val[$key];
@@ -452,7 +456,7 @@ class memcached
$this->stats['get_multi']++;
$sock_keys = array();
-
+
foreach ($keys as $key)
{
$sock = $this->get_sock($key);
@@ -494,7 +498,7 @@ class memcached
if ($this->_debug)
foreach ($val as $k => $v)
- $this->_debugprint(sprintf("MemCache: got %s => %s\r\n", $k, $v));
+ $this->_debugprint(sprintf("MemCache: got %s\n", $k));
return $val;
}
@@ -904,12 +908,12 @@ class memcached
return false;
}
- if ($this->_have_zlib && $flags & MEMCACHE_COMPRESSED)
+ if ($this->_have_zlib && $flags & memcached::COMPRESSED)
$ret[$rkey] = gzuncompress($ret[$rkey]);
$ret[$rkey] = rtrim($ret[$rkey]);
- if ($flags & MEMCACHE_SERIALIZED)
+ if ($flags & memcached::SERIALIZED)
$ret[$rkey] = unserialize($ret[$rkey]);
} else
@@ -950,7 +954,7 @@ class memcached
if (!is_scalar($val))
{
$val = serialize($val);
- $flags |= MEMCACHE_SERIALIZED;
+ $flags |= memcached::SERIALIZED;
if ($this->_debug)
$this->_debugprint(sprintf("client: serializing data as it is not scalar\n"));
}
@@ -963,13 +967,13 @@ class memcached
$c_val = gzcompress($val, 9);
$c_len = strlen($c_val);
- if ($c_len < $len*(1 - COMPRESSION_SAVINGS))
+ if ($c_len < $len*(1 - memcached::COMPRESSION_SAVINGS))
{
if ($this->_debug)
$this->_debugprint(sprintf("client: compressing data; was %d bytes is now %d bytes\n", $len, $c_len));
$val = $c_val;
$len = $c_len;
- $flags |= MEMCACHE_COMPRESSED;
+ $flags |= memcached::COMPRESSED;
}
}
if (!$this->_safe_fwrite($sock, "$cmd $key $flags $exp $len\r\n$val\r\n"))
@@ -979,9 +983,7 @@ class memcached
if ($this->_debug)
{
- if ($flags & MEMCACHE_COMPRESSED)
- $val = 'compressed data';
- $this->_debugprint(sprintf("MemCache: %s %s => %s (%s)\n", $cmd, $key, $val, $line));
+ $this->_debugprint(sprintf("%s %s (%s)\n", $cmd, $key, $line));
}
if ($line == "STORED")
return true;
@@ -1085,4 +1087,3 @@ class memcached
// vim: sts=3 sw=3 et
// }}}
-
diff --git a/includes/mime.info b/includes/mime.info
index dd3af7d0..63b38f5a 100644
--- a/includes/mime.info
+++ b/includes/mime.info
@@ -1,9 +1,9 @@
-#Mime type info file.
-#the first mime type in each line is the "main" mime type,
-#the others are aliases for this type
-#the media type is given in upper case and square brackets,
-#like [BITMAP], and must indicate a media type as defined by
-#the MEDIATYPE_xxx constants in Defines.php
+# Mime type info file.
+# the first mime type in each line is the "main" mime type,
+# the others are aliases for this type
+# the media type is given in upper case and square brackets,
+# like [BITMAP], and must indicate a media type as defined by
+# the MEDIATYPE_xxx constants in Defines.php
image/gif [BITMAP]
@@ -19,11 +19,12 @@ image/x-portable-graymap image/x-portable-greymap [BITMAP]
image/x-bmp image/bmp application/x-bmp application/bmp [BITMAP]
image/x-photoshop image/psd image/x-psd image/photoshop [BITMAP]
image/vnd.djvu image/x.djvu image/x-djvu [BITMAP]
-
+
image/svg+xml application/svg+xml application/svg image/svg [DRAWING]
application/postscript [DRAWING]
application/x-latex [DRAWING]
application/x-tex [DRAWING]
+application/x-dia-diagram [DRAWING]
audio/mp3 audio/mpeg3 audio/mpeg [AUDIO]
diff --git a/includes/mime.types b/includes/mime.types
index 64f77c12..6021e926 100644
--- a/includes/mime.types
+++ b/includes/mime.types
@@ -25,6 +25,7 @@ application/x-cdlink vcd
application/x-chess-pgn pgn
application/x-cpio cpio
application/x-csh csh
+application/x-dia-diagram dia
application/x-director dcr dir dxr
application/x-dvi dvi
application/x-futuresplash spl
@@ -57,14 +58,14 @@ application/xhtml+xml xhtml xht
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/zip zip jar xpi sxc stc sxd std sxi sti sxm stm sxw stw
audio/basic au snd
audio/midi mid midi kar
audio/mpeg mpga mp2 mp3
-audio/ogg ogg
+audio/ogg ogg
audio/x-aiff aif aiff aifc
audio/x-mpegurl m3u
-audio/x-ogg ogg
+audio/x-ogg ogg
audio/x-pn-realaudio ram rm
audio/x-pn-realaudio-plugin rpm
audio/x-realaudio ra
@@ -115,4 +116,4 @@ video/x-flv flv
video/x-msvideo avi
video/x-ogg ogm ogg
video/x-sgi-movie movie
-x-conference/x-cooltalk ice \ No newline at end of file
+x-conference/x-cooltalk ice
diff --git a/includes/normal/CleanUpTest.php b/includes/normal/CleanUpTest.php
index 0ca45b3c..d14bcad1 100644
--- a/includes/normal/CleanUpTest.php
+++ b/includes/normal/CleanUpTest.php
@@ -39,7 +39,7 @@ require_once 'UtfNormal.php';
* regression checks for known problems.
* Requires PHPUnit.
*
- * @addtogroup UtfNormal
+ * @ingroup UtfNormal
* @private
*/
class CleanUpTest extends PHPUnit_Framework_TestCase {
@@ -410,4 +410,3 @@ if( !$result->wasSuccessful() ) {
exit( -1 );
}
exit( 0 );
-
diff --git a/includes/normal/Makefile b/includes/normal/Makefile
index 887f3ce6..69ff3da1 100644
--- a/includes/normal/Makefile
+++ b/includes/normal/Makefile
@@ -5,8 +5,8 @@
## when the data was generated from a previous version.
#BASE=http://www.unicode.org/Public/UNIDATA
-# Explicitly using Unicode 5.0
-BASE=http://www.unicode.org/Public/5.0.0/ucd/
+# Explicitly using Unicode 5.1
+BASE=http://www.unicode.org/Public/5.1.0/ucd
# Can override to php-cli or php5 or whatevah
PHP=php
@@ -16,11 +16,14 @@ PHP=php
FETCH=wget
#FETCH=fetch
-all : UtfNormalData.inc
+all : UtfNormalData.inc Utf8Case.php
UtfNormalData.inc : UtfNormalGenerate.php UtfNormalUtil.php UnicodeData.txt CompositionExclusions.txt NormalizationCorrections.txt DerivedNormalizationProps.txt
$(PHP) UtfNormalGenerate.php
+Utf8Case.php : Utf8CaseGenerate.php UtfNormalUtil.php UnicodeData.txt
+ $(PHP) Utf8CaseGenerate.php
+
test : testutf8 testclean UtfNormalTest.php UtfNormalData.inc NormalizationTest.txt
$(PHP) UtfNormalTest.php
diff --git a/includes/normal/RandomTest.php b/includes/normal/RandomTest.php
index aa491dbb..018910cd 100644
--- a/includes/normal/RandomTest.php
+++ b/includes/normal/RandomTest.php
@@ -22,7 +22,7 @@
* UtfNormal::cleanUp() code paths, and checks to see if there's a
* difference. Will run forever until it finds one or you kill it.
*
- * @addtogroup UtfNormal
+ * @ingroup UtfNormal
* @access private
*/
@@ -104,5 +104,3 @@ while( true ) {
$clean = '';
$norm = '';
}
-
-
diff --git a/includes/normal/Utf8Case.php b/includes/normal/Utf8Case.php
new file mode 100644
index 00000000..9734a922
--- /dev/null
+++ b/includes/normal/Utf8Case.php
@@ -0,0 +1,2078 @@
+<?php
+/**
+ * Simple 1:1 upper/lowercase switching arrays for utf-8 text
+ * Won't get context-sensitive things yet
+ *
+ * Hack for bugs in ucfirst() and company
+ *
+ * These are pulled from memcached if possible, as this is faster than filling
+ * up a big array manually.
+ * @ingroup Language
+ */
+
+/*
+ * Translation array to get upper case character
+ */
+
+$wikiUpperChars = array(
+ 'a' => 'A',
+ 'b' => 'B',
+ 'c' => 'C',
+ 'd' => 'D',
+ 'e' => 'E',
+ 'f' => 'F',
+ 'g' => 'G',
+ 'h' => 'H',
+ 'i' => 'I',
+ 'j' => 'J',
+ 'k' => 'K',
+ 'l' => 'L',
+ 'm' => 'M',
+ 'n' => 'N',
+ 'o' => 'O',
+ 'p' => 'P',
+ 'q' => 'Q',
+ 'r' => 'R',
+ 's' => 'S',
+ 't' => 'T',
+ 'u' => 'U',
+ 'v' => 'V',
+ 'w' => 'W',
+ 'x' => 'X',
+ 'y' => 'Y',
+ 'z' => 'Z',
+ 'µ' => 'Μ',
+ 'à' => 'À',
+ 'á' => 'Á',
+ 'â' => 'Â',
+ 'ã' => 'Ã',
+ 'ä' => 'Ä',
+ 'å' => 'Å',
+ 'æ' => 'Æ',
+ 'ç' => 'Ç',
+ 'è' => 'È',
+ 'é' => 'É',
+ 'ê' => 'Ê',
+ 'ë' => 'Ë',
+ 'ì' => 'Ì',
+ 'í' => 'Í',
+ 'î' => 'Î',
+ 'ï' => 'Ï',
+ 'ð' => 'Ð',
+ 'ñ' => 'Ñ',
+ 'ò' => 'Ò',
+ 'ó' => 'Ó',
+ 'ô' => 'Ô',
+ 'õ' => 'Õ',
+ 'ö' => 'Ö',
+ 'ø' => 'Ø',
+ 'ù' => 'Ù',
+ 'ú' => 'Ú',
+ 'û' => 'Û',
+ 'ü' => 'Ü',
+ 'ý' => 'Ý',
+ 'þ' => 'Þ',
+ 'ÿ' => 'Ÿ',
+ 'ā' => 'Ā',
+ 'ă' => 'Ă',
+ 'ą' => 'Ą',
+ 'ć' => 'Ć',
+ 'ĉ' => 'Ĉ',
+ 'ċ' => 'Ċ',
+ 'č' => 'Č',
+ 'ď' => 'Ď',
+ 'đ' => 'Đ',
+ 'ē' => 'Ē',
+ 'ĕ' => 'Ĕ',
+ 'ė' => 'Ė',
+ 'ę' => 'Ę',
+ 'ě' => 'Ě',
+ 'ĝ' => 'Ĝ',
+ 'ğ' => 'Ğ',
+ 'ġ' => 'Ġ',
+ 'ģ' => 'Ģ',
+ 'ĥ' => 'Ĥ',
+ 'ħ' => 'Ħ',
+ 'ĩ' => 'Ĩ',
+ 'ī' => 'Ī',
+ 'ĭ' => 'Ĭ',
+ 'į' => 'Į',
+ 'ı' => 'I',
+ 'ij' => 'IJ',
+ 'ĵ' => 'Ĵ',
+ 'ķ' => 'Ķ',
+ 'ĺ' => 'Ĺ',
+ 'ļ' => 'Ļ',
+ 'ľ' => 'Ľ',
+ 'ŀ' => 'Ŀ',
+ 'ł' => 'Ł',
+ 'ń' => 'Ń',
+ 'ņ' => 'Ņ',
+ 'ň' => 'Ň',
+ 'ŋ' => 'Ŋ',
+ 'ō' => 'Ō',
+ 'ŏ' => 'Ŏ',
+ 'ő' => 'Ő',
+ 'œ' => 'Œ',
+ 'ŕ' => 'Ŕ',
+ 'ŗ' => 'Ŗ',
+ 'ř' => 'Ř',
+ 'ś' => 'Ś',
+ 'ŝ' => 'Ŝ',
+ 'ş' => 'Ş',
+ 'š' => 'Š',
+ 'ţ' => 'Ţ',
+ 'ť' => 'Ť',
+ 'ŧ' => 'Ŧ',
+ 'ũ' => 'Ũ',
+ 'ū' => 'Ū',
+ 'ŭ' => 'Ŭ',
+ 'ů' => 'Ů',
+ 'ű' => 'Ű',
+ 'ų' => 'Ų',
+ 'ŵ' => 'Ŵ',
+ 'ŷ' => 'Ŷ',
+ 'ź' => 'Ź',
+ 'ż' => 'Ż',
+ 'ž' => 'Ž',
+ 'ſ' => 'S',
+ 'ƀ' => 'Ƀ',
+ 'ƃ' => 'Ƃ',
+ 'ƅ' => 'Ƅ',
+ 'ƈ' => 'Ƈ',
+ 'ƌ' => 'Ƌ',
+ 'ƒ' => 'Ƒ',
+ 'ƕ' => 'Ƕ',
+ 'ƙ' => 'Ƙ',
+ 'ƚ' => 'Ƚ',
+ 'ƞ' => 'Ƞ',
+ 'ơ' => 'Ơ',
+ 'ƣ' => 'Ƣ',
+ 'ƥ' => 'Ƥ',
+ 'ƨ' => 'Ƨ',
+ 'ƭ' => 'Ƭ',
+ 'ư' => 'Ư',
+ 'ƴ' => 'Ƴ',
+ 'ƶ' => 'Ƶ',
+ 'ƹ' => 'Ƹ',
+ 'ƽ' => 'Ƽ',
+ 'ƿ' => 'Ƿ',
+ 'Dž' => 'DŽ',
+ 'dž' => 'DŽ',
+ 'Lj' => 'LJ',
+ 'lj' => 'LJ',
+ 'Nj' => 'NJ',
+ 'nj' => 'NJ',
+ 'ǎ' => 'Ǎ',
+ 'ǐ' => 'Ǐ',
+ 'ǒ' => 'Ǒ',
+ 'ǔ' => 'Ǔ',
+ 'ǖ' => 'Ǖ',
+ 'ǘ' => 'Ǘ',
+ 'ǚ' => 'Ǚ',
+ 'ǜ' => 'Ǜ',
+ 'ǝ' => 'Ǝ',
+ 'ǟ' => 'Ǟ',
+ 'ǡ' => 'Ǡ',
+ 'ǣ' => 'Ǣ',
+ 'ǥ' => 'Ǥ',
+ 'ǧ' => 'Ǧ',
+ 'ǩ' => 'Ǩ',
+ 'ǫ' => 'Ǫ',
+ 'ǭ' => 'Ǭ',
+ 'ǯ' => 'Ǯ',
+ 'Dz' => 'DZ',
+ 'dz' => 'DZ',
+ 'ǵ' => 'Ǵ',
+ 'ǹ' => 'Ǹ',
+ 'ǻ' => 'Ǻ',
+ 'ǽ' => 'Ǽ',
+ 'ǿ' => 'Ǿ',
+ 'ȁ' => 'Ȁ',
+ 'ȃ' => 'Ȃ',
+ 'ȅ' => 'Ȅ',
+ 'ȇ' => 'Ȇ',
+ 'ȉ' => 'Ȉ',
+ 'ȋ' => 'Ȋ',
+ 'ȍ' => 'Ȍ',
+ 'ȏ' => 'Ȏ',
+ 'ȑ' => 'Ȑ',
+ 'ȓ' => 'Ȓ',
+ 'ȕ' => 'Ȕ',
+ 'ȗ' => 'Ȗ',
+ 'ș' => 'Ș',
+ 'ț' => 'Ț',
+ 'ȝ' => 'Ȝ',
+ 'ȟ' => 'Ȟ',
+ 'ȣ' => 'Ȣ',
+ 'ȥ' => 'Ȥ',
+ 'ȧ' => 'Ȧ',
+ 'ȩ' => 'Ȩ',
+ 'ȫ' => 'Ȫ',
+ 'ȭ' => 'Ȭ',
+ 'ȯ' => 'Ȯ',
+ 'ȱ' => 'Ȱ',
+ 'ȳ' => 'Ȳ',
+ 'ȼ' => 'Ȼ',
+ 'ɂ' => 'Ɂ',
+ 'ɇ' => 'Ɇ',
+ 'ɉ' => 'Ɉ',
+ 'ɋ' => 'Ɋ',
+ 'ɍ' => 'Ɍ',
+ 'ɏ' => 'Ɏ',
+ 'ɐ' => 'Ɐ',
+ 'ɑ' => 'Ɑ',
+ 'ɓ' => 'Ɓ',
+ 'ɔ' => 'Ɔ',
+ 'ɖ' => 'Ɖ',
+ 'ɗ' => 'Ɗ',
+ 'ə' => 'Ə',
+ 'ɛ' => 'Ɛ',
+ 'ɠ' => 'Ɠ',
+ 'ɣ' => 'Ɣ',
+ 'ɨ' => 'Ɨ',
+ 'ɩ' => 'Ɩ',
+ 'ɫ' => 'Ɫ',
+ 'ɯ' => 'Ɯ',
+ 'ɱ' => 'Ɱ',
+ 'ɲ' => 'Ɲ',
+ 'ɵ' => 'Ɵ',
+ 'ɽ' => 'Ɽ',
+ 'ʀ' => 'Ʀ',
+ 'ʃ' => 'Ʃ',
+ 'ʈ' => 'Ʈ',
+ 'ʉ' => 'Ʉ',
+ 'ʊ' => 'Ʊ',
+ 'ʋ' => 'Ʋ',
+ 'ʌ' => 'Ʌ',
+ 'ʒ' => 'Ʒ',
+ 'ͅ' => 'Ι',
+ 'ͱ' => 'Ͱ',
+ 'ͳ' => 'Ͳ',
+ 'ͷ' => 'Ͷ',
+ 'ͻ' => 'Ͻ',
+ 'ͼ' => 'Ͼ',
+ 'ͽ' => 'Ͽ',
+ 'ά' => 'Ά',
+ 'έ' => 'Έ',
+ 'ή' => 'Ή',
+ 'ί' => 'Ί',
+ 'α' => 'Α',
+ 'β' => 'Β',
+ 'γ' => 'Γ',
+ 'δ' => 'Δ',
+ 'ε' => 'Ε',
+ 'ζ' => 'Ζ',
+ 'η' => 'Η',
+ 'θ' => 'Θ',
+ 'ι' => 'Ι',
+ 'κ' => 'Κ',
+ 'λ' => 'Λ',
+ 'μ' => 'Μ',
+ 'ν' => 'Ν',
+ 'ξ' => 'Ξ',
+ 'ο' => 'Ο',
+ 'π' => 'Π',
+ 'ρ' => 'Ρ',
+ 'ς' => 'Σ',
+ 'σ' => 'Σ',
+ 'τ' => 'Τ',
+ 'υ' => 'Υ',
+ 'φ' => 'Φ',
+ 'χ' => 'Χ',
+ 'ψ' => 'Ψ',
+ 'ω' => 'Ω',
+ 'ϊ' => 'Ϊ',
+ 'ϋ' => 'Ϋ',
+ 'ό' => 'Ό',
+ 'ύ' => 'Ύ',
+ 'ώ' => 'Ώ',
+ 'ϐ' => 'Β',
+ 'ϑ' => 'Θ',
+ 'ϕ' => 'Φ',
+ 'ϖ' => 'Π',
+ 'ϗ' => 'Ϗ',
+ 'ϙ' => 'Ϙ',
+ 'ϛ' => 'Ϛ',
+ 'ϝ' => 'Ϝ',
+ 'ϟ' => 'Ϟ',
+ 'ϡ' => 'Ϡ',
+ 'ϣ' => 'Ϣ',
+ 'ϥ' => 'Ϥ',
+ 'ϧ' => 'Ϧ',
+ 'ϩ' => 'Ϩ',
+ 'ϫ' => 'Ϫ',
+ 'ϭ' => 'Ϭ',
+ 'ϯ' => 'Ϯ',
+ 'ϰ' => 'Κ',
+ 'ϱ' => 'Ρ',
+ 'ϲ' => 'Ϲ',
+ 'ϵ' => 'Ε',
+ 'ϸ' => 'Ϸ',
+ 'ϻ' => 'Ϻ',
+ 'а' => 'А',
+ 'б' => 'Б',
+ 'в' => 'В',
+ 'г' => 'Г',
+ 'д' => 'Д',
+ 'е' => 'Е',
+ 'ж' => 'Ж',
+ 'з' => 'З',
+ 'и' => 'И',
+ 'й' => 'Й',
+ 'к' => 'К',
+ 'л' => 'Л',
+ 'м' => 'М',
+ 'н' => 'Н',
+ 'о' => 'О',
+ 'п' => 'П',
+ 'р' => 'Р',
+ 'с' => 'С',
+ 'т' => 'Т',
+ 'у' => 'У',
+ 'ф' => 'Ф',
+ 'х' => 'Х',
+ 'ц' => 'Ц',
+ 'ч' => 'Ч',
+ 'ш' => 'Ш',
+ 'щ' => 'Щ',
+ 'ъ' => 'Ъ',
+ 'ы' => 'Ы',
+ 'ь' => 'Ь',
+ 'э' => 'Э',
+ 'ю' => 'Ю',
+ 'я' => 'Я',
+ 'ѐ' => 'Ѐ',
+ 'ё' => 'Ё',
+ 'ђ' => 'Ђ',
+ 'ѓ' => 'Ѓ',
+ 'є' => 'Є',
+ 'ѕ' => 'Ѕ',
+ 'і' => 'І',
+ 'ї' => 'Ї',
+ 'ј' => 'Ј',
+ 'љ' => 'Љ',
+ 'њ' => 'Њ',
+ 'ћ' => 'Ћ',
+ 'ќ' => 'Ќ',
+ 'ѝ' => 'Ѝ',
+ 'ў' => 'Ў',
+ 'џ' => 'Џ',
+ 'ѡ' => 'Ѡ',
+ 'ѣ' => 'Ѣ',
+ 'ѥ' => 'Ѥ',
+ 'ѧ' => 'Ѧ',
+ 'ѩ' => 'Ѩ',
+ 'ѫ' => 'Ѫ',
+ 'ѭ' => 'Ѭ',
+ 'ѯ' => 'Ѯ',
+ 'ѱ' => 'Ѱ',
+ 'ѳ' => 'Ѳ',
+ 'ѵ' => 'Ѵ',
+ 'ѷ' => 'Ѷ',
+ 'ѹ' => 'Ѹ',
+ 'ѻ' => 'Ѻ',
+ 'ѽ' => 'Ѽ',
+ 'ѿ' => 'Ѿ',
+ 'ҁ' => 'Ҁ',
+ 'ҋ' => 'Ҋ',
+ 'ҍ' => 'Ҍ',
+ 'ҏ' => 'Ҏ',
+ 'ґ' => 'Ґ',
+ 'ғ' => 'Ғ',
+ 'ҕ' => 'Ҕ',
+ 'җ' => 'Җ',
+ 'ҙ' => 'Ҙ',
+ 'қ' => 'Қ',
+ 'ҝ' => 'Ҝ',
+ 'ҟ' => 'Ҟ',
+ 'ҡ' => 'Ҡ',
+ 'ң' => 'Ң',
+ 'ҥ' => 'Ҥ',
+ 'ҧ' => 'Ҧ',
+ 'ҩ' => 'Ҩ',
+ 'ҫ' => 'Ҫ',
+ 'ҭ' => 'Ҭ',
+ 'ү' => 'Ү',
+ 'ұ' => 'Ұ',
+ 'ҳ' => 'Ҳ',
+ 'ҵ' => 'Ҵ',
+ 'ҷ' => 'Ҷ',
+ 'ҹ' => 'Ҹ',
+ 'һ' => 'Һ',
+ 'ҽ' => 'Ҽ',
+ 'ҿ' => 'Ҿ',
+ 'ӂ' => 'Ӂ',
+ 'ӄ' => 'Ӄ',
+ 'ӆ' => 'Ӆ',
+ 'ӈ' => 'Ӈ',
+ 'ӊ' => 'Ӊ',
+ 'ӌ' => 'Ӌ',
+ 'ӎ' => 'Ӎ',
+ 'ӏ' => 'Ӏ',
+ 'ӑ' => 'Ӑ',
+ 'ӓ' => 'Ӓ',
+ 'ӕ' => 'Ӕ',
+ 'ӗ' => 'Ӗ',
+ 'ә' => 'Ә',
+ 'ӛ' => 'Ӛ',
+ 'ӝ' => 'Ӝ',
+ 'ӟ' => 'Ӟ',
+ 'ӡ' => 'Ӡ',
+ 'ӣ' => 'Ӣ',
+ 'ӥ' => 'Ӥ',
+ 'ӧ' => 'Ӧ',
+ 'ө' => 'Ө',
+ 'ӫ' => 'Ӫ',
+ 'ӭ' => 'Ӭ',
+ 'ӯ' => 'Ӯ',
+ 'ӱ' => 'Ӱ',
+ 'ӳ' => 'Ӳ',
+ 'ӵ' => 'Ӵ',
+ 'ӷ' => 'Ӷ',
+ 'ӹ' => 'Ӹ',
+ 'ӻ' => 'Ӻ',
+ 'ӽ' => 'Ӽ',
+ 'ӿ' => 'Ӿ',
+ 'ԁ' => 'Ԁ',
+ 'ԃ' => 'Ԃ',
+ 'ԅ' => 'Ԅ',
+ 'ԇ' => 'Ԇ',
+ 'ԉ' => 'Ԉ',
+ 'ԋ' => 'Ԋ',
+ 'ԍ' => 'Ԍ',
+ 'ԏ' => 'Ԏ',
+ 'ԑ' => 'Ԑ',
+ 'ԓ' => 'Ԓ',
+ 'ԕ' => 'Ԕ',
+ 'ԗ' => 'Ԗ',
+ 'ԙ' => 'Ԙ',
+ 'ԛ' => 'Ԛ',
+ 'ԝ' => 'Ԝ',
+ 'ԟ' => 'Ԟ',
+ 'ԡ' => 'Ԡ',
+ 'ԣ' => 'Ԣ',
+ 'ա' => 'Ա',
+ 'բ' => 'Բ',
+ 'գ' => 'Գ',
+ 'դ' => 'Դ',
+ 'ե' => 'Ե',
+ 'զ' => 'Զ',
+ 'է' => 'Է',
+ 'ը' => 'Ը',
+ 'թ' => 'Թ',
+ 'ժ' => 'Ժ',
+ 'ի' => 'Ի',
+ 'լ' => 'Լ',
+ 'խ' => 'Խ',
+ 'ծ' => 'Ծ',
+ 'կ' => 'Կ',
+ 'հ' => 'Հ',
+ 'ձ' => 'Ձ',
+ 'ղ' => 'Ղ',
+ 'ճ' => 'Ճ',
+ 'մ' => 'Մ',
+ 'յ' => 'Յ',
+ 'ն' => 'Ն',
+ 'շ' => 'Շ',
+ 'ո' => 'Ո',
+ 'չ' => 'Չ',
+ 'պ' => 'Պ',
+ 'ջ' => 'Ջ',
+ 'ռ' => 'Ռ',
+ 'ս' => 'Ս',
+ 'վ' => 'Վ',
+ 'տ' => 'Տ',
+ 'ր' => 'Ր',
+ 'ց' => 'Ց',
+ 'ւ' => 'Ւ',
+ 'փ' => 'Փ',
+ 'ք' => 'Ք',
+ 'օ' => 'Օ',
+ 'ֆ' => 'Ֆ',
+ 'ᵹ' => 'Ᵹ',
+ 'ᵽ' => 'Ᵽ',
+ 'ḁ' => 'Ḁ',
+ 'ḃ' => 'Ḃ',
+ 'ḅ' => 'Ḅ',
+ 'ḇ' => 'Ḇ',
+ 'ḉ' => 'Ḉ',
+ 'ḋ' => 'Ḋ',
+ 'ḍ' => 'Ḍ',
+ 'ḏ' => 'Ḏ',
+ 'ḑ' => 'Ḑ',
+ 'ḓ' => 'Ḓ',
+ 'ḕ' => 'Ḕ',
+ 'ḗ' => 'Ḗ',
+ 'ḙ' => 'Ḙ',
+ 'ḛ' => 'Ḛ',
+ 'ḝ' => 'Ḝ',
+ 'ḟ' => 'Ḟ',
+ 'ḡ' => 'Ḡ',
+ 'ḣ' => 'Ḣ',
+ 'ḥ' => 'Ḥ',
+ 'ḧ' => 'Ḧ',
+ 'ḩ' => 'Ḩ',
+ 'ḫ' => 'Ḫ',
+ 'ḭ' => 'Ḭ',
+ 'ḯ' => 'Ḯ',
+ 'ḱ' => 'Ḱ',
+ 'ḳ' => 'Ḳ',
+ 'ḵ' => 'Ḵ',
+ 'ḷ' => 'Ḷ',
+ 'ḹ' => 'Ḹ',
+ 'ḻ' => 'Ḻ',
+ 'ḽ' => 'Ḽ',
+ 'ḿ' => 'Ḿ',
+ 'ṁ' => 'Ṁ',
+ 'ṃ' => 'Ṃ',
+ 'ṅ' => 'Ṅ',
+ 'ṇ' => 'Ṇ',
+ 'ṉ' => 'Ṉ',
+ 'ṋ' => 'Ṋ',
+ 'ṍ' => 'Ṍ',
+ 'ṏ' => 'Ṏ',
+ 'ṑ' => 'Ṑ',
+ 'ṓ' => 'Ṓ',
+ 'ṕ' => 'Ṕ',
+ 'ṗ' => 'Ṗ',
+ 'ṙ' => 'Ṙ',
+ 'ṛ' => 'Ṛ',
+ 'ṝ' => 'Ṝ',
+ 'ṟ' => 'Ṟ',
+ 'ṡ' => 'Ṡ',
+ 'ṣ' => 'Ṣ',
+ 'ṥ' => 'Ṥ',
+ 'ṧ' => 'Ṧ',
+ 'ṩ' => 'Ṩ',
+ 'ṫ' => 'Ṫ',
+ 'ṭ' => 'Ṭ',
+ 'ṯ' => 'Ṯ',
+ 'ṱ' => 'Ṱ',
+ 'ṳ' => 'Ṳ',
+ 'ṵ' => 'Ṵ',
+ 'ṷ' => 'Ṷ',
+ 'ṹ' => 'Ṹ',
+ 'ṻ' => 'Ṻ',
+ 'ṽ' => 'Ṽ',
+ 'ṿ' => 'Ṿ',
+ 'ẁ' => 'Ẁ',
+ 'ẃ' => 'Ẃ',
+ 'ẅ' => 'Ẅ',
+ 'ẇ' => 'Ẇ',
+ 'ẉ' => 'Ẉ',
+ 'ẋ' => 'Ẋ',
+ 'ẍ' => 'Ẍ',
+ 'ẏ' => 'Ẏ',
+ 'ẑ' => 'Ẑ',
+ 'ẓ' => 'Ẓ',
+ 'ẕ' => 'Ẕ',
+ 'ẛ' => 'Ṡ',
+ 'ạ' => 'Ạ',
+ 'ả' => 'Ả',
+ 'ấ' => 'Ấ',
+ 'ầ' => 'Ầ',
+ 'ẩ' => 'Ẩ',
+ 'ẫ' => 'Ẫ',
+ 'ậ' => 'Ậ',
+ 'ắ' => 'Ắ',
+ 'ằ' => 'Ằ',
+ 'ẳ' => 'Ẳ',
+ 'ẵ' => 'Ẵ',
+ 'ặ' => 'Ặ',
+ 'ẹ' => 'Ẹ',
+ 'ẻ' => 'Ẻ',
+ 'ẽ' => 'Ẽ',
+ 'ế' => 'Ế',
+ 'ề' => 'Ề',
+ 'ể' => 'Ể',
+ 'ễ' => 'Ễ',
+ 'ệ' => 'Ệ',
+ 'ỉ' => 'Ỉ',
+ 'ị' => 'Ị',
+ 'ọ' => 'Ọ',
+ 'ỏ' => 'Ỏ',
+ 'ố' => 'Ố',
+ 'ồ' => 'Ồ',
+ 'ổ' => 'Ổ',
+ 'ỗ' => 'Ỗ',
+ 'ộ' => 'Ộ',
+ 'ớ' => 'Ớ',
+ 'ờ' => 'Ờ',
+ 'ở' => 'Ở',
+ 'ỡ' => 'Ỡ',
+ 'ợ' => 'Ợ',
+ 'ụ' => 'Ụ',
+ 'ủ' => 'Ủ',
+ 'ứ' => 'Ứ',
+ 'ừ' => 'Ừ',
+ 'ử' => 'Ử',
+ 'ữ' => 'Ữ',
+ 'ự' => 'Ự',
+ 'ỳ' => 'Ỳ',
+ 'ỵ' => 'Ỵ',
+ 'ỷ' => 'Ỷ',
+ 'ỹ' => 'Ỹ',
+ 'ỻ' => 'Ỻ',
+ 'ỽ' => 'Ỽ',
+ 'ỿ' => 'Ỿ',
+ 'ἀ' => 'Ἀ',
+ 'ἁ' => 'Ἁ',
+ 'ἂ' => 'Ἂ',
+ 'ἃ' => 'Ἃ',
+ 'ἄ' => 'Ἄ',
+ 'ἅ' => 'Ἅ',
+ 'ἆ' => 'Ἆ',
+ 'ἇ' => 'Ἇ',
+ 'ἐ' => 'Ἐ',
+ 'ἑ' => 'Ἑ',
+ 'ἒ' => 'Ἒ',
+ 'ἓ' => 'Ἓ',
+ 'ἔ' => 'Ἔ',
+ 'ἕ' => 'Ἕ',
+ 'ἠ' => 'Ἠ',
+ 'ἡ' => 'Ἡ',
+ 'ἢ' => 'Ἢ',
+ 'ἣ' => 'Ἣ',
+ 'ἤ' => 'Ἤ',
+ 'ἥ' => 'Ἥ',
+ 'ἦ' => 'Ἦ',
+ 'ἧ' => 'Ἧ',
+ 'ἰ' => 'Ἰ',
+ 'ἱ' => 'Ἱ',
+ 'ἲ' => 'Ἲ',
+ 'ἳ' => 'Ἳ',
+ 'ἴ' => 'Ἴ',
+ 'ἵ' => 'Ἵ',
+ 'ἶ' => 'Ἶ',
+ 'ἷ' => 'Ἷ',
+ 'ὀ' => 'Ὀ',
+ 'ὁ' => 'Ὁ',
+ 'ὂ' => 'Ὂ',
+ 'ὃ' => 'Ὃ',
+ 'ὄ' => 'Ὄ',
+ 'ὅ' => 'Ὅ',
+ 'ὑ' => 'Ὑ',
+ 'ὓ' => 'Ὓ',
+ 'ὕ' => 'Ὕ',
+ 'ὗ' => 'Ὗ',
+ 'ὠ' => 'Ὠ',
+ 'ὡ' => 'Ὡ',
+ 'ὢ' => 'Ὢ',
+ 'ὣ' => 'Ὣ',
+ 'ὤ' => 'Ὤ',
+ 'ὥ' => 'Ὥ',
+ 'ὦ' => 'Ὦ',
+ 'ὧ' => 'Ὧ',
+ 'ὰ' => 'Ὰ',
+ 'ά' => 'Ά',
+ 'ὲ' => 'Ὲ',
+ 'έ' => 'Έ',
+ 'ὴ' => 'Ὴ',
+ 'ή' => 'Ή',
+ 'ὶ' => 'Ὶ',
+ 'ί' => 'Ί',
+ 'ὸ' => 'Ὸ',
+ 'ό' => 'Ό',
+ 'ὺ' => 'Ὺ',
+ 'ύ' => 'Ύ',
+ 'ὼ' => 'Ὼ',
+ 'ώ' => 'Ώ',
+ 'ᾀ' => 'ᾈ',
+ 'ᾁ' => 'ᾉ',
+ 'ᾂ' => 'ᾊ',
+ 'ᾃ' => 'ᾋ',
+ 'ᾄ' => 'ᾌ',
+ 'ᾅ' => 'ᾍ',
+ 'ᾆ' => 'ᾎ',
+ 'ᾇ' => 'ᾏ',
+ 'ᾐ' => 'ᾘ',
+ 'ᾑ' => 'ᾙ',
+ 'ᾒ' => 'ᾚ',
+ 'ᾓ' => 'ᾛ',
+ 'ᾔ' => 'ᾜ',
+ 'ᾕ' => 'ᾝ',
+ 'ᾖ' => 'ᾞ',
+ 'ᾗ' => 'ᾟ',
+ 'ᾠ' => 'ᾨ',
+ 'ᾡ' => 'ᾩ',
+ 'ᾢ' => 'ᾪ',
+ 'ᾣ' => 'ᾫ',
+ 'ᾤ' => 'ᾬ',
+ 'ᾥ' => 'ᾭ',
+ 'ᾦ' => 'ᾮ',
+ 'ᾧ' => 'ᾯ',
+ 'ᾰ' => 'Ᾰ',
+ 'ᾱ' => 'Ᾱ',
+ 'ᾳ' => 'ᾼ',
+ 'ι' => 'Ι',
+ 'ῃ' => 'ῌ',
+ 'ῐ' => 'Ῐ',
+ 'ῑ' => 'Ῑ',
+ 'ῠ' => 'Ῠ',
+ 'ῡ' => 'Ῡ',
+ 'ῥ' => 'Ῥ',
+ 'ῳ' => 'ῼ',
+ 'ⅎ' => 'Ⅎ',
+ 'ⅰ' => 'Ⅰ',
+ 'ⅱ' => 'Ⅱ',
+ 'ⅲ' => 'Ⅲ',
+ 'ⅳ' => 'Ⅳ',
+ 'ⅴ' => 'Ⅴ',
+ 'ⅵ' => 'Ⅵ',
+ 'ⅶ' => 'Ⅶ',
+ 'ⅷ' => 'Ⅷ',
+ 'ⅸ' => 'Ⅸ',
+ 'ⅹ' => 'Ⅹ',
+ 'ⅺ' => 'Ⅺ',
+ 'ⅻ' => 'Ⅻ',
+ 'ⅼ' => 'Ⅼ',
+ 'ⅽ' => 'Ⅽ',
+ 'ⅾ' => 'Ⅾ',
+ 'ⅿ' => 'Ⅿ',
+ 'ↄ' => 'Ↄ',
+ 'ⓐ' => 'Ⓐ',
+ 'ⓑ' => 'Ⓑ',
+ 'ⓒ' => 'Ⓒ',
+ 'ⓓ' => 'Ⓓ',
+ 'ⓔ' => 'Ⓔ',
+ 'ⓕ' => 'Ⓕ',
+ 'ⓖ' => 'Ⓖ',
+ 'ⓗ' => 'Ⓗ',
+ 'ⓘ' => 'Ⓘ',
+ 'ⓙ' => 'Ⓙ',
+ 'ⓚ' => 'Ⓚ',
+ 'ⓛ' => 'Ⓛ',
+ 'ⓜ' => 'Ⓜ',
+ 'ⓝ' => 'Ⓝ',
+ 'ⓞ' => 'Ⓞ',
+ 'ⓟ' => 'Ⓟ',
+ 'ⓠ' => 'Ⓠ',
+ 'ⓡ' => 'Ⓡ',
+ 'ⓢ' => 'Ⓢ',
+ 'ⓣ' => 'Ⓣ',
+ 'ⓤ' => 'Ⓤ',
+ 'ⓥ' => 'Ⓥ',
+ 'ⓦ' => 'Ⓦ',
+ 'ⓧ' => 'Ⓧ',
+ 'ⓨ' => 'Ⓨ',
+ 'ⓩ' => 'Ⓩ',
+ 'ⰰ' => 'Ⰰ',
+ 'ⰱ' => 'Ⰱ',
+ 'ⰲ' => 'Ⰲ',
+ 'ⰳ' => 'Ⰳ',
+ 'ⰴ' => 'Ⰴ',
+ 'ⰵ' => 'Ⰵ',
+ 'ⰶ' => 'Ⰶ',
+ 'ⰷ' => 'Ⰷ',
+ 'ⰸ' => 'Ⰸ',
+ 'ⰹ' => 'Ⰹ',
+ 'ⰺ' => 'Ⰺ',
+ 'ⰻ' => 'Ⰻ',
+ 'ⰼ' => 'Ⰼ',
+ 'ⰽ' => 'Ⰽ',
+ 'ⰾ' => 'Ⰾ',
+ 'ⰿ' => 'Ⰿ',
+ 'ⱀ' => 'Ⱀ',
+ 'ⱁ' => 'Ⱁ',
+ 'ⱂ' => 'Ⱂ',
+ 'ⱃ' => 'Ⱃ',
+ 'ⱄ' => 'Ⱄ',
+ 'ⱅ' => 'Ⱅ',
+ 'ⱆ' => 'Ⱆ',
+ 'ⱇ' => 'Ⱇ',
+ 'ⱈ' => 'Ⱈ',
+ 'ⱉ' => 'Ⱉ',
+ 'ⱊ' => 'Ⱊ',
+ 'ⱋ' => 'Ⱋ',
+ 'ⱌ' => 'Ⱌ',
+ 'ⱍ' => 'Ⱍ',
+ 'ⱎ' => 'Ⱎ',
+ 'ⱏ' => 'Ⱏ',
+ 'ⱐ' => 'Ⱐ',
+ 'ⱑ' => 'Ⱑ',
+ 'ⱒ' => 'Ⱒ',
+ 'ⱓ' => 'Ⱓ',
+ 'ⱔ' => 'Ⱔ',
+ 'ⱕ' => 'Ⱕ',
+ 'ⱖ' => 'Ⱖ',
+ 'ⱗ' => 'Ⱗ',
+ 'ⱘ' => 'Ⱘ',
+ 'ⱙ' => 'Ⱙ',
+ 'ⱚ' => 'Ⱚ',
+ 'ⱛ' => 'Ⱛ',
+ 'ⱜ' => 'Ⱜ',
+ 'ⱝ' => 'Ⱝ',
+ 'ⱞ' => 'Ⱞ',
+ 'ⱡ' => 'Ⱡ',
+ 'ⱥ' => 'Ⱥ',
+ 'ⱦ' => 'Ⱦ',
+ 'ⱨ' => 'Ⱨ',
+ 'ⱪ' => 'Ⱪ',
+ 'ⱬ' => 'Ⱬ',
+ 'ⱳ' => 'Ⱳ',
+ 'ⱶ' => 'Ⱶ',
+ 'ⲁ' => 'Ⲁ',
+ 'ⲃ' => 'Ⲃ',
+ 'ⲅ' => 'Ⲅ',
+ 'ⲇ' => 'Ⲇ',
+ 'ⲉ' => 'Ⲉ',
+ 'ⲋ' => 'Ⲋ',
+ 'ⲍ' => 'Ⲍ',
+ 'ⲏ' => 'Ⲏ',
+ 'ⲑ' => 'Ⲑ',
+ 'ⲓ' => 'Ⲓ',
+ 'ⲕ' => 'Ⲕ',
+ 'ⲗ' => 'Ⲗ',
+ 'ⲙ' => 'Ⲙ',
+ 'ⲛ' => 'Ⲛ',
+ 'ⲝ' => 'Ⲝ',
+ 'ⲟ' => 'Ⲟ',
+ 'ⲡ' => 'Ⲡ',
+ 'ⲣ' => 'Ⲣ',
+ 'ⲥ' => 'Ⲥ',
+ 'ⲧ' => 'Ⲧ',
+ 'ⲩ' => 'Ⲩ',
+ 'ⲫ' => 'Ⲫ',
+ 'ⲭ' => 'Ⲭ',
+ 'ⲯ' => 'Ⲯ',
+ 'ⲱ' => 'Ⲱ',
+ 'ⲳ' => 'Ⲳ',
+ 'ⲵ' => 'Ⲵ',
+ 'ⲷ' => 'Ⲷ',
+ 'ⲹ' => 'Ⲹ',
+ 'ⲻ' => 'Ⲻ',
+ 'ⲽ' => 'Ⲽ',
+ 'ⲿ' => 'Ⲿ',
+ 'ⳁ' => 'Ⳁ',
+ 'ⳃ' => 'Ⳃ',
+ 'ⳅ' => 'Ⳅ',
+ 'ⳇ' => 'Ⳇ',
+ 'ⳉ' => 'Ⳉ',
+ 'ⳋ' => 'Ⳋ',
+ 'ⳍ' => 'Ⳍ',
+ 'ⳏ' => 'Ⳏ',
+ 'ⳑ' => 'Ⳑ',
+ 'ⳓ' => 'Ⳓ',
+ 'ⳕ' => 'Ⳕ',
+ 'ⳗ' => 'Ⳗ',
+ 'ⳙ' => 'Ⳙ',
+ 'ⳛ' => 'Ⳛ',
+ 'ⳝ' => 'Ⳝ',
+ 'ⳟ' => 'Ⳟ',
+ 'ⳡ' => 'Ⳡ',
+ 'ⳣ' => 'Ⳣ',
+ 'ⴀ' => 'Ⴀ',
+ 'ⴁ' => 'Ⴁ',
+ 'ⴂ' => 'Ⴂ',
+ 'ⴃ' => 'Ⴃ',
+ 'ⴄ' => 'Ⴄ',
+ 'ⴅ' => 'Ⴅ',
+ 'ⴆ' => 'Ⴆ',
+ 'ⴇ' => 'Ⴇ',
+ 'ⴈ' => 'Ⴈ',
+ 'ⴉ' => 'Ⴉ',
+ 'ⴊ' => 'Ⴊ',
+ 'ⴋ' => 'Ⴋ',
+ 'ⴌ' => 'Ⴌ',
+ 'ⴍ' => 'Ⴍ',
+ 'ⴎ' => 'Ⴎ',
+ 'ⴏ' => 'Ⴏ',
+ 'ⴐ' => 'Ⴐ',
+ 'ⴑ' => 'Ⴑ',
+ 'ⴒ' => 'Ⴒ',
+ 'ⴓ' => 'Ⴓ',
+ 'ⴔ' => 'Ⴔ',
+ 'ⴕ' => 'Ⴕ',
+ 'ⴖ' => 'Ⴖ',
+ 'ⴗ' => 'Ⴗ',
+ 'ⴘ' => 'Ⴘ',
+ 'ⴙ' => 'Ⴙ',
+ 'ⴚ' => 'Ⴚ',
+ 'ⴛ' => 'Ⴛ',
+ 'ⴜ' => 'Ⴜ',
+ 'ⴝ' => 'Ⴝ',
+ 'ⴞ' => 'Ⴞ',
+ 'ⴟ' => 'Ⴟ',
+ 'ⴠ' => 'Ⴠ',
+ 'ⴡ' => 'Ⴡ',
+ 'ⴢ' => 'Ⴢ',
+ 'ⴣ' => 'Ⴣ',
+ 'ⴤ' => 'Ⴤ',
+ 'ⴥ' => 'Ⴥ',
+ 'ꙁ' => 'Ꙁ',
+ 'ꙃ' => 'Ꙃ',
+ 'ꙅ' => 'Ꙅ',
+ 'ꙇ' => 'Ꙇ',
+ 'ꙉ' => 'Ꙉ',
+ 'ꙋ' => 'Ꙋ',
+ 'ꙍ' => 'Ꙍ',
+ 'ꙏ' => 'Ꙏ',
+ 'ꙑ' => 'Ꙑ',
+ 'ꙓ' => 'Ꙓ',
+ 'ꙕ' => 'Ꙕ',
+ 'ꙗ' => 'Ꙗ',
+ 'ꙙ' => 'Ꙙ',
+ 'ꙛ' => 'Ꙛ',
+ 'ꙝ' => 'Ꙝ',
+ 'ꙟ' => 'Ꙟ',
+ 'ꙣ' => 'Ꙣ',
+ 'ꙥ' => 'Ꙥ',
+ 'ꙧ' => 'Ꙧ',
+ 'ꙩ' => 'Ꙩ',
+ 'ꙫ' => 'Ꙫ',
+ 'ꙭ' => 'Ꙭ',
+ 'ꚁ' => 'Ꚁ',
+ 'ꚃ' => 'Ꚃ',
+ 'ꚅ' => 'Ꚅ',
+ 'ꚇ' => 'Ꚇ',
+ 'ꚉ' => 'Ꚉ',
+ 'ꚋ' => 'Ꚋ',
+ 'ꚍ' => 'Ꚍ',
+ 'ꚏ' => 'Ꚏ',
+ 'ꚑ' => 'Ꚑ',
+ 'ꚓ' => 'Ꚓ',
+ 'ꚕ' => 'Ꚕ',
+ 'ꚗ' => 'Ꚗ',
+ 'ꜣ' => 'Ꜣ',
+ 'ꜥ' => 'Ꜥ',
+ 'ꜧ' => 'Ꜧ',
+ 'ꜩ' => 'Ꜩ',
+ 'ꜫ' => 'Ꜫ',
+ 'ꜭ' => 'Ꜭ',
+ 'ꜯ' => 'Ꜯ',
+ 'ꜳ' => 'Ꜳ',
+ 'ꜵ' => 'Ꜵ',
+ 'ꜷ' => 'Ꜷ',
+ 'ꜹ' => 'Ꜹ',
+ 'ꜻ' => 'Ꜻ',
+ 'ꜽ' => 'Ꜽ',
+ 'ꜿ' => 'Ꜿ',
+ 'ꝁ' => 'Ꝁ',
+ 'ꝃ' => 'Ꝃ',
+ 'ꝅ' => 'Ꝅ',
+ 'ꝇ' => 'Ꝇ',
+ 'ꝉ' => 'Ꝉ',
+ 'ꝋ' => 'Ꝋ',
+ 'ꝍ' => 'Ꝍ',
+ 'ꝏ' => 'Ꝏ',
+ 'ꝑ' => 'Ꝑ',
+ 'ꝓ' => 'Ꝓ',
+ 'ꝕ' => 'Ꝕ',
+ 'ꝗ' => 'Ꝗ',
+ 'ꝙ' => 'Ꝙ',
+ 'ꝛ' => 'Ꝛ',
+ 'ꝝ' => 'Ꝝ',
+ 'ꝟ' => 'Ꝟ',
+ 'ꝡ' => 'Ꝡ',
+ 'ꝣ' => 'Ꝣ',
+ 'ꝥ' => 'Ꝥ',
+ 'ꝧ' => 'Ꝧ',
+ 'ꝩ' => 'Ꝩ',
+ 'ꝫ' => 'Ꝫ',
+ 'ꝭ' => 'Ꝭ',
+ 'ꝯ' => 'Ꝯ',
+ 'ꝺ' => 'Ꝺ',
+ 'ꝼ' => 'Ꝼ',
+ 'ꝿ' => 'Ꝿ',
+ 'ꞁ' => 'Ꞁ',
+ 'ꞃ' => 'Ꞃ',
+ 'ꞅ' => 'Ꞅ',
+ 'ꞇ' => 'Ꞇ',
+ 'ꞌ' => 'Ꞌ',
+ 'a' => 'A',
+ 'b' => 'B',
+ 'c' => 'C',
+ 'd' => 'D',
+ 'e' => 'E',
+ 'f' => 'F',
+ 'g' => 'G',
+ 'h' => 'H',
+ 'i' => 'I',
+ 'j' => 'J',
+ 'k' => 'K',
+ 'l' => 'L',
+ 'm' => 'M',
+ 'n' => 'N',
+ 'o' => 'O',
+ 'p' => 'P',
+ 'q' => 'Q',
+ 'r' => 'R',
+ 's' => 'S',
+ 't' => 'T',
+ 'u' => 'U',
+ 'v' => 'V',
+ 'w' => 'W',
+ 'x' => 'X',
+ 'y' => 'Y',
+ 'z' => 'Z',
+ '𐐨' => '𐐀',
+ '𐐩' => '𐐁',
+ '𐐪' => '𐐂',
+ '𐐫' => '𐐃',
+ '𐐬' => '𐐄',
+ '𐐭' => '𐐅',
+ '𐐮' => '𐐆',
+ '𐐯' => '𐐇',
+ '𐐰' => '𐐈',
+ '𐐱' => '𐐉',
+ '𐐲' => '𐐊',
+ '𐐳' => '𐐋',
+ '𐐴' => '𐐌',
+ '𐐵' => '𐐍',
+ '𐐶' => '𐐎',
+ '𐐷' => '𐐏',
+ '𐐸' => '𐐐',
+ '𐐹' => '𐐑',
+ '𐐺' => '𐐒',
+ '𐐻' => '𐐓',
+ '𐐼' => '𐐔',
+ '𐐽' => '𐐕',
+ '𐐾' => '𐐖',
+ '𐐿' => '𐐗',
+ '𐑀' => '𐐘',
+ '𐑁' => '𐐙',
+ '𐑂' => '𐐚',
+ '𐑃' => '𐐛',
+ '𐑄' => '𐐜',
+ '𐑅' => '𐐝',
+ '𐑆' => '𐐞',
+ '𐑇' => '𐐟',
+ '𐑈' => '𐐠',
+ '𐑉' => '𐐡',
+ '𐑊' => '𐐢',
+ '𐑋' => '𐐣',
+ '𐑌' => '𐐤',
+ '𐑍' => '𐐥',
+ '𐑎' => '𐐦',
+ '𐑏' => '𐐧'
+);
+
+/*
+ * Translation array to get lower case character
+ */
+$wikiLowerChars = array(
+ 'A' => 'a',
+ 'B' => 'b',
+ 'C' => 'c',
+ 'D' => 'd',
+ 'E' => 'e',
+ 'F' => 'f',
+ 'G' => 'g',
+ 'H' => 'h',
+ 'I' => 'i',
+ 'J' => 'j',
+ 'K' => 'k',
+ 'L' => 'l',
+ 'M' => 'm',
+ 'N' => 'n',
+ 'O' => 'o',
+ 'P' => 'p',
+ 'Q' => 'q',
+ 'R' => 'r',
+ 'S' => 's',
+ 'T' => 't',
+ 'U' => 'u',
+ 'V' => 'v',
+ 'W' => 'w',
+ 'X' => 'x',
+ 'Y' => 'y',
+ 'Z' => 'z',
+ 'À' => 'à',
+ 'Á' => 'á',
+ 'Â' => 'â',
+ 'Ã' => 'ã',
+ 'Ä' => 'ä',
+ 'Å' => 'å',
+ 'Æ' => 'æ',
+ 'Ç' => 'ç',
+ 'È' => 'è',
+ 'É' => 'é',
+ 'Ê' => 'ê',
+ 'Ë' => 'ë',
+ 'Ì' => 'ì',
+ 'Í' => 'í',
+ 'Î' => 'î',
+ 'Ï' => 'ï',
+ 'Ð' => 'ð',
+ 'Ñ' => 'ñ',
+ 'Ò' => 'ò',
+ 'Ó' => 'ó',
+ 'Ô' => 'ô',
+ 'Õ' => 'õ',
+ 'Ö' => 'ö',
+ 'Ø' => 'ø',
+ 'Ù' => 'ù',
+ 'Ú' => 'ú',
+ 'Û' => 'û',
+ 'Ü' => 'ü',
+ 'Ý' => 'ý',
+ 'Þ' => 'þ',
+ 'Ā' => 'ā',
+ 'Ă' => 'ă',
+ 'Ą' => 'ą',
+ 'Ć' => 'ć',
+ 'Ĉ' => 'ĉ',
+ 'Ċ' => 'ċ',
+ 'Č' => 'č',
+ 'Ď' => 'ď',
+ 'Đ' => 'đ',
+ 'Ē' => 'ē',
+ 'Ĕ' => 'ĕ',
+ 'Ė' => 'ė',
+ 'Ę' => 'ę',
+ 'Ě' => 'ě',
+ 'Ĝ' => 'ĝ',
+ 'Ğ' => 'ğ',
+ 'Ġ' => 'ġ',
+ 'Ģ' => 'ģ',
+ 'Ĥ' => 'ĥ',
+ 'Ħ' => 'ħ',
+ 'Ĩ' => 'ĩ',
+ 'Ī' => 'ī',
+ 'Ĭ' => 'ĭ',
+ 'Į' => 'į',
+ 'İ' => 'i',
+ 'IJ' => 'ij',
+ 'Ĵ' => 'ĵ',
+ 'Ķ' => 'ķ',
+ 'Ĺ' => 'ĺ',
+ 'Ļ' => 'ļ',
+ 'Ľ' => 'ľ',
+ 'Ŀ' => 'ŀ',
+ 'Ł' => 'ł',
+ 'Ń' => 'ń',
+ 'Ņ' => 'ņ',
+ 'Ň' => 'ň',
+ 'Ŋ' => 'ŋ',
+ 'Ō' => 'ō',
+ 'Ŏ' => 'ŏ',
+ 'Ő' => 'ő',
+ 'Œ' => 'œ',
+ 'Ŕ' => 'ŕ',
+ 'Ŗ' => 'ŗ',
+ 'Ř' => 'ř',
+ 'Ś' => 'ś',
+ 'Ŝ' => 'ŝ',
+ 'Ş' => 'ş',
+ 'Š' => 'š',
+ 'Ţ' => 'ţ',
+ 'Ť' => 'ť',
+ 'Ŧ' => 'ŧ',
+ 'Ũ' => 'ũ',
+ 'Ū' => 'ū',
+ 'Ŭ' => 'ŭ',
+ 'Ů' => 'ů',
+ 'Ű' => 'ű',
+ 'Ų' => 'ų',
+ 'Ŵ' => 'ŵ',
+ 'Ŷ' => 'ŷ',
+ 'Ÿ' => 'ÿ',
+ 'Ź' => 'ź',
+ 'Ż' => 'ż',
+ 'Ž' => 'ž',
+ 'Ɓ' => 'ɓ',
+ 'Ƃ' => 'ƃ',
+ 'Ƅ' => 'ƅ',
+ 'Ɔ' => 'ɔ',
+ 'Ƈ' => 'ƈ',
+ 'Ɖ' => 'ɖ',
+ 'Ɗ' => 'ɗ',
+ 'Ƌ' => 'ƌ',
+ 'Ǝ' => 'ǝ',
+ 'Ə' => 'ə',
+ 'Ɛ' => 'ɛ',
+ 'Ƒ' => 'ƒ',
+ 'Ɠ' => 'ɠ',
+ 'Ɣ' => 'ɣ',
+ 'Ɩ' => 'ɩ',
+ 'Ɨ' => 'ɨ',
+ 'Ƙ' => 'ƙ',
+ 'Ɯ' => 'ɯ',
+ 'Ɲ' => 'ɲ',
+ 'Ɵ' => 'ɵ',
+ 'Ơ' => 'ơ',
+ 'Ƣ' => 'ƣ',
+ 'Ƥ' => 'ƥ',
+ 'Ʀ' => 'ʀ',
+ 'Ƨ' => 'ƨ',
+ 'Ʃ' => 'ʃ',
+ 'Ƭ' => 'ƭ',
+ 'Ʈ' => 'ʈ',
+ 'Ư' => 'ư',
+ 'Ʊ' => 'ʊ',
+ 'Ʋ' => 'ʋ',
+ 'Ƴ' => 'ƴ',
+ 'Ƶ' => 'ƶ',
+ 'Ʒ' => 'ʒ',
+ 'Ƹ' => 'ƹ',
+ 'Ƽ' => 'ƽ',
+ 'DŽ' => 'dž',
+ 'Dž' => 'dž',
+ 'LJ' => 'lj',
+ 'Lj' => 'lj',
+ 'NJ' => 'nj',
+ 'Nj' => 'nj',
+ 'Ǎ' => 'ǎ',
+ 'Ǐ' => 'ǐ',
+ 'Ǒ' => 'ǒ',
+ 'Ǔ' => 'ǔ',
+ 'Ǖ' => 'ǖ',
+ 'Ǘ' => 'ǘ',
+ 'Ǚ' => 'ǚ',
+ 'Ǜ' => 'ǜ',
+ 'Ǟ' => 'ǟ',
+ 'Ǡ' => 'ǡ',
+ 'Ǣ' => 'ǣ',
+ 'Ǥ' => 'ǥ',
+ 'Ǧ' => 'ǧ',
+ 'Ǩ' => 'ǩ',
+ 'Ǫ' => 'ǫ',
+ 'Ǭ' => 'ǭ',
+ 'Ǯ' => 'ǯ',
+ 'DZ' => 'dz',
+ 'Dz' => 'dz',
+ 'Ǵ' => 'ǵ',
+ 'Ƕ' => 'ƕ',
+ 'Ƿ' => 'ƿ',
+ 'Ǹ' => 'ǹ',
+ 'Ǻ' => 'ǻ',
+ 'Ǽ' => 'ǽ',
+ 'Ǿ' => 'ǿ',
+ 'Ȁ' => 'ȁ',
+ 'Ȃ' => 'ȃ',
+ 'Ȅ' => 'ȅ',
+ 'Ȇ' => 'ȇ',
+ 'Ȉ' => 'ȉ',
+ 'Ȋ' => 'ȋ',
+ 'Ȍ' => 'ȍ',
+ 'Ȏ' => 'ȏ',
+ 'Ȑ' => 'ȑ',
+ 'Ȓ' => 'ȓ',
+ 'Ȕ' => 'ȕ',
+ 'Ȗ' => 'ȗ',
+ 'Ș' => 'ș',
+ 'Ț' => 'ț',
+ 'Ȝ' => 'ȝ',
+ 'Ȟ' => 'ȟ',
+ 'Ƞ' => 'ƞ',
+ 'Ȣ' => 'ȣ',
+ 'Ȥ' => 'ȥ',
+ 'Ȧ' => 'ȧ',
+ 'Ȩ' => 'ȩ',
+ 'Ȫ' => 'ȫ',
+ 'Ȭ' => 'ȭ',
+ 'Ȯ' => 'ȯ',
+ 'Ȱ' => 'ȱ',
+ 'Ȳ' => 'ȳ',
+ 'Ⱥ' => 'ⱥ',
+ 'Ȼ' => 'ȼ',
+ 'Ƚ' => 'ƚ',
+ 'Ⱦ' => 'ⱦ',
+ 'Ɂ' => 'ɂ',
+ 'Ƀ' => 'ƀ',
+ 'Ʉ' => 'ʉ',
+ 'Ʌ' => 'ʌ',
+ 'Ɇ' => 'ɇ',
+ 'Ɉ' => 'ɉ',
+ 'Ɋ' => 'ɋ',
+ 'Ɍ' => 'ɍ',
+ 'Ɏ' => 'ɏ',
+ 'Ͱ' => 'ͱ',
+ 'Ͳ' => 'ͳ',
+ 'Ͷ' => 'ͷ',
+ 'Ά' => 'ά',
+ 'Έ' => 'έ',
+ 'Ή' => 'ή',
+ 'Ί' => 'ί',
+ 'Ό' => 'ό',
+ 'Ύ' => 'ύ',
+ 'Ώ' => 'ώ',
+ 'Α' => 'α',
+ 'Β' => 'β',
+ 'Γ' => 'γ',
+ 'Δ' => 'δ',
+ 'Ε' => 'ε',
+ 'Ζ' => 'ζ',
+ 'Η' => 'η',
+ 'Θ' => 'θ',
+ 'Ι' => 'ι',
+ 'Κ' => 'κ',
+ 'Λ' => 'λ',
+ 'Μ' => 'μ',
+ 'Ν' => 'ν',
+ 'Ξ' => 'ξ',
+ 'Ο' => 'ο',
+ 'Π' => 'π',
+ 'Ρ' => 'ρ',
+ 'Σ' => 'σ',
+ 'Τ' => 'τ',
+ 'Υ' => 'υ',
+ 'Φ' => 'φ',
+ 'Χ' => 'χ',
+ 'Ψ' => 'ψ',
+ 'Ω' => 'ω',
+ 'Ϊ' => 'ϊ',
+ 'Ϋ' => 'ϋ',
+ 'Ϗ' => 'ϗ',
+ 'Ϙ' => 'ϙ',
+ 'Ϛ' => 'ϛ',
+ 'Ϝ' => 'ϝ',
+ 'Ϟ' => 'ϟ',
+ 'Ϡ' => 'ϡ',
+ 'Ϣ' => 'ϣ',
+ 'Ϥ' => 'ϥ',
+ 'Ϧ' => 'ϧ',
+ 'Ϩ' => 'ϩ',
+ 'Ϫ' => 'ϫ',
+ 'Ϭ' => 'ϭ',
+ 'Ϯ' => 'ϯ',
+ 'ϴ' => 'θ',
+ 'Ϸ' => 'ϸ',
+ 'Ϲ' => 'ϲ',
+ 'Ϻ' => 'ϻ',
+ 'Ͻ' => 'ͻ',
+ 'Ͼ' => 'ͼ',
+ 'Ͽ' => 'ͽ',
+ 'Ѐ' => 'ѐ',
+ 'Ё' => 'ё',
+ 'Ђ' => 'ђ',
+ 'Ѓ' => 'ѓ',
+ 'Є' => 'є',
+ 'Ѕ' => 'ѕ',
+ 'І' => 'і',
+ 'Ї' => 'ї',
+ 'Ј' => 'ј',
+ 'Љ' => 'љ',
+ 'Њ' => 'њ',
+ 'Ћ' => 'ћ',
+ 'Ќ' => 'ќ',
+ 'Ѝ' => 'ѝ',
+ 'Ў' => 'ў',
+ 'Џ' => 'џ',
+ 'А' => 'а',
+ 'Б' => 'б',
+ 'В' => 'в',
+ 'Г' => 'г',
+ 'Д' => 'д',
+ 'Е' => 'е',
+ 'Ж' => 'ж',
+ 'З' => 'з',
+ 'И' => 'и',
+ 'Й' => 'й',
+ 'К' => 'к',
+ 'Л' => 'л',
+ 'М' => 'м',
+ 'Н' => 'н',
+ 'О' => 'о',
+ 'П' => 'п',
+ 'Р' => 'р',
+ 'С' => 'с',
+ 'Т' => 'т',
+ 'У' => 'у',
+ 'Ф' => 'ф',
+ 'Х' => 'х',
+ 'Ц' => 'ц',
+ 'Ч' => 'ч',
+ 'Ш' => 'ш',
+ 'Щ' => 'щ',
+ 'Ъ' => 'ъ',
+ 'Ы' => 'ы',
+ 'Ь' => 'ь',
+ 'Э' => 'э',
+ 'Ю' => 'ю',
+ 'Я' => 'я',
+ 'Ѡ' => 'ѡ',
+ 'Ѣ' => 'ѣ',
+ 'Ѥ' => 'ѥ',
+ 'Ѧ' => 'ѧ',
+ 'Ѩ' => 'ѩ',
+ 'Ѫ' => 'ѫ',
+ 'Ѭ' => 'ѭ',
+ 'Ѯ' => 'ѯ',
+ 'Ѱ' => 'ѱ',
+ 'Ѳ' => 'ѳ',
+ 'Ѵ' => 'ѵ',
+ 'Ѷ' => 'ѷ',
+ 'Ѹ' => 'ѹ',
+ 'Ѻ' => 'ѻ',
+ 'Ѽ' => 'ѽ',
+ 'Ѿ' => 'ѿ',
+ 'Ҁ' => 'ҁ',
+ 'Ҋ' => 'ҋ',
+ 'Ҍ' => 'ҍ',
+ 'Ҏ' => 'ҏ',
+ 'Ґ' => 'ґ',
+ 'Ғ' => 'ғ',
+ 'Ҕ' => 'ҕ',
+ 'Җ' => 'җ',
+ 'Ҙ' => 'ҙ',
+ 'Қ' => 'қ',
+ 'Ҝ' => 'ҝ',
+ 'Ҟ' => 'ҟ',
+ 'Ҡ' => 'ҡ',
+ 'Ң' => 'ң',
+ 'Ҥ' => 'ҥ',
+ 'Ҧ' => 'ҧ',
+ 'Ҩ' => 'ҩ',
+ 'Ҫ' => 'ҫ',
+ 'Ҭ' => 'ҭ',
+ 'Ү' => 'ү',
+ 'Ұ' => 'ұ',
+ 'Ҳ' => 'ҳ',
+ 'Ҵ' => 'ҵ',
+ 'Ҷ' => 'ҷ',
+ 'Ҹ' => 'ҹ',
+ 'Һ' => 'һ',
+ 'Ҽ' => 'ҽ',
+ 'Ҿ' => 'ҿ',
+ 'Ӏ' => 'ӏ',
+ 'Ӂ' => 'ӂ',
+ 'Ӄ' => 'ӄ',
+ 'Ӆ' => 'ӆ',
+ 'Ӈ' => 'ӈ',
+ 'Ӊ' => 'ӊ',
+ 'Ӌ' => 'ӌ',
+ 'Ӎ' => 'ӎ',
+ 'Ӑ' => 'ӑ',
+ 'Ӓ' => 'ӓ',
+ 'Ӕ' => 'ӕ',
+ 'Ӗ' => 'ӗ',
+ 'Ә' => 'ә',
+ 'Ӛ' => 'ӛ',
+ 'Ӝ' => 'ӝ',
+ 'Ӟ' => 'ӟ',
+ 'Ӡ' => 'ӡ',
+ 'Ӣ' => 'ӣ',
+ 'Ӥ' => 'ӥ',
+ 'Ӧ' => 'ӧ',
+ 'Ө' => 'ө',
+ 'Ӫ' => 'ӫ',
+ 'Ӭ' => 'ӭ',
+ 'Ӯ' => 'ӯ',
+ 'Ӱ' => 'ӱ',
+ 'Ӳ' => 'ӳ',
+ 'Ӵ' => 'ӵ',
+ 'Ӷ' => 'ӷ',
+ 'Ӹ' => 'ӹ',
+ 'Ӻ' => 'ӻ',
+ 'Ӽ' => 'ӽ',
+ 'Ӿ' => 'ӿ',
+ 'Ԁ' => 'ԁ',
+ 'Ԃ' => 'ԃ',
+ 'Ԅ' => 'ԅ',
+ 'Ԇ' => 'ԇ',
+ 'Ԉ' => 'ԉ',
+ 'Ԋ' => 'ԋ',
+ 'Ԍ' => 'ԍ',
+ 'Ԏ' => 'ԏ',
+ 'Ԑ' => 'ԑ',
+ 'Ԓ' => 'ԓ',
+ 'Ԕ' => 'ԕ',
+ 'Ԗ' => 'ԗ',
+ 'Ԙ' => 'ԙ',
+ 'Ԛ' => 'ԛ',
+ 'Ԝ' => 'ԝ',
+ 'Ԟ' => 'ԟ',
+ 'Ԡ' => 'ԡ',
+ 'Ԣ' => 'ԣ',
+ 'Ա' => 'ա',
+ 'Բ' => 'բ',
+ 'Գ' => 'գ',
+ 'Դ' => 'դ',
+ 'Ե' => 'ե',
+ 'Զ' => 'զ',
+ 'Է' => 'է',
+ 'Ը' => 'ը',
+ 'Թ' => 'թ',
+ 'Ժ' => 'ժ',
+ 'Ի' => 'ի',
+ 'Լ' => 'լ',
+ 'Խ' => 'խ',
+ 'Ծ' => 'ծ',
+ 'Կ' => 'կ',
+ 'Հ' => 'հ',
+ 'Ձ' => 'ձ',
+ 'Ղ' => 'ղ',
+ 'Ճ' => 'ճ',
+ 'Մ' => 'մ',
+ 'Յ' => 'յ',
+ 'Ն' => 'ն',
+ 'Շ' => 'շ',
+ 'Ո' => 'ո',
+ 'Չ' => 'չ',
+ 'Պ' => 'պ',
+ 'Ջ' => 'ջ',
+ 'Ռ' => 'ռ',
+ 'Ս' => 'ս',
+ 'Վ' => 'վ',
+ 'Տ' => 'տ',
+ 'Ր' => 'ր',
+ 'Ց' => 'ց',
+ 'Ւ' => 'ւ',
+ 'Փ' => 'փ',
+ 'Ք' => 'ք',
+ 'Օ' => 'օ',
+ 'Ֆ' => 'ֆ',
+ 'Ⴀ' => 'ⴀ',
+ 'Ⴁ' => 'ⴁ',
+ 'Ⴂ' => 'ⴂ',
+ 'Ⴃ' => 'ⴃ',
+ 'Ⴄ' => 'ⴄ',
+ 'Ⴅ' => 'ⴅ',
+ 'Ⴆ' => 'ⴆ',
+ 'Ⴇ' => 'ⴇ',
+ 'Ⴈ' => 'ⴈ',
+ 'Ⴉ' => 'ⴉ',
+ 'Ⴊ' => 'ⴊ',
+ 'Ⴋ' => 'ⴋ',
+ 'Ⴌ' => 'ⴌ',
+ 'Ⴍ' => 'ⴍ',
+ 'Ⴎ' => 'ⴎ',
+ 'Ⴏ' => 'ⴏ',
+ 'Ⴐ' => 'ⴐ',
+ 'Ⴑ' => 'ⴑ',
+ 'Ⴒ' => 'ⴒ',
+ 'Ⴓ' => 'ⴓ',
+ 'Ⴔ' => 'ⴔ',
+ 'Ⴕ' => 'ⴕ',
+ 'Ⴖ' => 'ⴖ',
+ 'Ⴗ' => 'ⴗ',
+ 'Ⴘ' => 'ⴘ',
+ 'Ⴙ' => 'ⴙ',
+ 'Ⴚ' => 'ⴚ',
+ 'Ⴛ' => 'ⴛ',
+ 'Ⴜ' => 'ⴜ',
+ 'Ⴝ' => 'ⴝ',
+ 'Ⴞ' => 'ⴞ',
+ 'Ⴟ' => 'ⴟ',
+ 'Ⴠ' => 'ⴠ',
+ 'Ⴡ' => 'ⴡ',
+ 'Ⴢ' => 'ⴢ',
+ 'Ⴣ' => 'ⴣ',
+ 'Ⴤ' => 'ⴤ',
+ 'Ⴥ' => 'ⴥ',
+ 'Ḁ' => 'ḁ',
+ 'Ḃ' => 'ḃ',
+ 'Ḅ' => 'ḅ',
+ 'Ḇ' => 'ḇ',
+ 'Ḉ' => 'ḉ',
+ 'Ḋ' => 'ḋ',
+ 'Ḍ' => 'ḍ',
+ 'Ḏ' => 'ḏ',
+ 'Ḑ' => 'ḑ',
+ 'Ḓ' => 'ḓ',
+ 'Ḕ' => 'ḕ',
+ 'Ḗ' => 'ḗ',
+ 'Ḙ' => 'ḙ',
+ 'Ḛ' => 'ḛ',
+ 'Ḝ' => 'ḝ',
+ 'Ḟ' => 'ḟ',
+ 'Ḡ' => 'ḡ',
+ 'Ḣ' => 'ḣ',
+ 'Ḥ' => 'ḥ',
+ 'Ḧ' => 'ḧ',
+ 'Ḩ' => 'ḩ',
+ 'Ḫ' => 'ḫ',
+ 'Ḭ' => 'ḭ',
+ 'Ḯ' => 'ḯ',
+ 'Ḱ' => 'ḱ',
+ 'Ḳ' => 'ḳ',
+ 'Ḵ' => 'ḵ',
+ 'Ḷ' => 'ḷ',
+ 'Ḹ' => 'ḹ',
+ 'Ḻ' => 'ḻ',
+ 'Ḽ' => 'ḽ',
+ 'Ḿ' => 'ḿ',
+ 'Ṁ' => 'ṁ',
+ 'Ṃ' => 'ṃ',
+ 'Ṅ' => 'ṅ',
+ 'Ṇ' => 'ṇ',
+ 'Ṉ' => 'ṉ',
+ 'Ṋ' => 'ṋ',
+ 'Ṍ' => 'ṍ',
+ 'Ṏ' => 'ṏ',
+ 'Ṑ' => 'ṑ',
+ 'Ṓ' => 'ṓ',
+ 'Ṕ' => 'ṕ',
+ 'Ṗ' => 'ṗ',
+ 'Ṙ' => 'ṙ',
+ 'Ṛ' => 'ṛ',
+ 'Ṝ' => 'ṝ',
+ 'Ṟ' => 'ṟ',
+ 'Ṡ' => 'ṡ',
+ 'Ṣ' => 'ṣ',
+ 'Ṥ' => 'ṥ',
+ 'Ṧ' => 'ṧ',
+ 'Ṩ' => 'ṩ',
+ 'Ṫ' => 'ṫ',
+ 'Ṭ' => 'ṭ',
+ 'Ṯ' => 'ṯ',
+ 'Ṱ' => 'ṱ',
+ 'Ṳ' => 'ṳ',
+ 'Ṵ' => 'ṵ',
+ 'Ṷ' => 'ṷ',
+ 'Ṹ' => 'ṹ',
+ 'Ṻ' => 'ṻ',
+ 'Ṽ' => 'ṽ',
+ 'Ṿ' => 'ṿ',
+ 'Ẁ' => 'ẁ',
+ 'Ẃ' => 'ẃ',
+ 'Ẅ' => 'ẅ',
+ 'Ẇ' => 'ẇ',
+ 'Ẉ' => 'ẉ',
+ 'Ẋ' => 'ẋ',
+ 'Ẍ' => 'ẍ',
+ 'Ẏ' => 'ẏ',
+ 'Ẑ' => 'ẑ',
+ 'Ẓ' => 'ẓ',
+ 'Ẕ' => 'ẕ',
+ 'ẞ' => 'ß',
+ 'Ạ' => 'ạ',
+ 'Ả' => 'ả',
+ 'Ấ' => 'ấ',
+ 'Ầ' => 'ầ',
+ 'Ẩ' => 'ẩ',
+ 'Ẫ' => 'ẫ',
+ 'Ậ' => 'ậ',
+ 'Ắ' => 'ắ',
+ 'Ằ' => 'ằ',
+ 'Ẳ' => 'ẳ',
+ 'Ẵ' => 'ẵ',
+ 'Ặ' => 'ặ',
+ 'Ẹ' => 'ẹ',
+ 'Ẻ' => 'ẻ',
+ 'Ẽ' => 'ẽ',
+ 'Ế' => 'ế',
+ 'Ề' => 'ề',
+ 'Ể' => 'ể',
+ 'Ễ' => 'ễ',
+ 'Ệ' => 'ệ',
+ 'Ỉ' => 'ỉ',
+ 'Ị' => 'ị',
+ 'Ọ' => 'ọ',
+ 'Ỏ' => 'ỏ',
+ 'Ố' => 'ố',
+ 'Ồ' => 'ồ',
+ 'Ổ' => 'ổ',
+ 'Ỗ' => 'ỗ',
+ 'Ộ' => 'ộ',
+ 'Ớ' => 'ớ',
+ 'Ờ' => 'ờ',
+ 'Ở' => 'ở',
+ 'Ỡ' => 'ỡ',
+ 'Ợ' => 'ợ',
+ 'Ụ' => 'ụ',
+ 'Ủ' => 'ủ',
+ 'Ứ' => 'ứ',
+ 'Ừ' => 'ừ',
+ 'Ử' => 'ử',
+ 'Ữ' => 'ữ',
+ 'Ự' => 'ự',
+ 'Ỳ' => 'ỳ',
+ 'Ỵ' => 'ỵ',
+ 'Ỷ' => 'ỷ',
+ 'Ỹ' => 'ỹ',
+ 'Ỻ' => 'ỻ',
+ 'Ỽ' => 'ỽ',
+ 'Ỿ' => 'ỿ',
+ 'Ἀ' => 'ἀ',
+ 'Ἁ' => 'ἁ',
+ 'Ἂ' => 'ἂ',
+ 'Ἃ' => 'ἃ',
+ 'Ἄ' => 'ἄ',
+ 'Ἅ' => 'ἅ',
+ 'Ἆ' => 'ἆ',
+ 'Ἇ' => 'ἇ',
+ 'Ἐ' => 'ἐ',
+ 'Ἑ' => 'ἑ',
+ 'Ἒ' => 'ἒ',
+ 'Ἓ' => 'ἓ',
+ 'Ἔ' => 'ἔ',
+ 'Ἕ' => 'ἕ',
+ 'Ἠ' => 'ἠ',
+ 'Ἡ' => 'ἡ',
+ 'Ἢ' => 'ἢ',
+ 'Ἣ' => 'ἣ',
+ 'Ἤ' => 'ἤ',
+ 'Ἥ' => 'ἥ',
+ 'Ἦ' => 'ἦ',
+ 'Ἧ' => 'ἧ',
+ 'Ἰ' => 'ἰ',
+ 'Ἱ' => 'ἱ',
+ 'Ἲ' => 'ἲ',
+ 'Ἳ' => 'ἳ',
+ 'Ἴ' => 'ἴ',
+ 'Ἵ' => 'ἵ',
+ 'Ἶ' => 'ἶ',
+ 'Ἷ' => 'ἷ',
+ 'Ὀ' => 'ὀ',
+ 'Ὁ' => 'ὁ',
+ 'Ὂ' => 'ὂ',
+ 'Ὃ' => 'ὃ',
+ 'Ὄ' => 'ὄ',
+ 'Ὅ' => 'ὅ',
+ 'Ὑ' => 'ὑ',
+ 'Ὓ' => 'ὓ',
+ 'Ὕ' => 'ὕ',
+ 'Ὗ' => 'ὗ',
+ 'Ὠ' => 'ὠ',
+ 'Ὡ' => 'ὡ',
+ 'Ὢ' => 'ὢ',
+ 'Ὣ' => 'ὣ',
+ 'Ὤ' => 'ὤ',
+ 'Ὥ' => 'ὥ',
+ 'Ὦ' => 'ὦ',
+ 'Ὧ' => 'ὧ',
+ 'ᾈ' => 'ᾀ',
+ 'ᾉ' => 'ᾁ',
+ 'ᾊ' => 'ᾂ',
+ 'ᾋ' => 'ᾃ',
+ 'ᾌ' => 'ᾄ',
+ 'ᾍ' => 'ᾅ',
+ 'ᾎ' => 'ᾆ',
+ 'ᾏ' => 'ᾇ',
+ 'ᾘ' => 'ᾐ',
+ 'ᾙ' => 'ᾑ',
+ 'ᾚ' => 'ᾒ',
+ 'ᾛ' => 'ᾓ',
+ 'ᾜ' => 'ᾔ',
+ 'ᾝ' => 'ᾕ',
+ 'ᾞ' => 'ᾖ',
+ 'ᾟ' => 'ᾗ',
+ 'ᾨ' => 'ᾠ',
+ 'ᾩ' => 'ᾡ',
+ 'ᾪ' => 'ᾢ',
+ 'ᾫ' => 'ᾣ',
+ 'ᾬ' => 'ᾤ',
+ 'ᾭ' => 'ᾥ',
+ 'ᾮ' => 'ᾦ',
+ 'ᾯ' => 'ᾧ',
+ 'Ᾰ' => 'ᾰ',
+ 'Ᾱ' => 'ᾱ',
+ 'Ὰ' => 'ὰ',
+ 'Ά' => 'ά',
+ 'ᾼ' => 'ᾳ',
+ 'Ὲ' => 'ὲ',
+ 'Έ' => 'έ',
+ 'Ὴ' => 'ὴ',
+ 'Ή' => 'ή',
+ 'ῌ' => 'ῃ',
+ 'Ῐ' => 'ῐ',
+ 'Ῑ' => 'ῑ',
+ 'Ὶ' => 'ὶ',
+ 'Ί' => 'ί',
+ 'Ῠ' => 'ῠ',
+ 'Ῡ' => 'ῡ',
+ 'Ὺ' => 'ὺ',
+ 'Ύ' => 'ύ',
+ 'Ῥ' => 'ῥ',
+ 'Ὸ' => 'ὸ',
+ 'Ό' => 'ό',
+ 'Ὼ' => 'ὼ',
+ 'Ώ' => 'ώ',
+ 'ῼ' => 'ῳ',
+ 'Ω' => 'ω',
+ 'K' => 'k',
+ 'Å' => 'å',
+ 'Ⅎ' => 'ⅎ',
+ 'Ⅰ' => 'ⅰ',
+ 'Ⅱ' => 'ⅱ',
+ 'Ⅲ' => 'ⅲ',
+ 'Ⅳ' => 'ⅳ',
+ 'Ⅴ' => 'ⅴ',
+ 'Ⅵ' => 'ⅵ',
+ 'Ⅶ' => 'ⅶ',
+ 'Ⅷ' => 'ⅷ',
+ 'Ⅸ' => 'ⅸ',
+ 'Ⅹ' => 'ⅹ',
+ 'Ⅺ' => 'ⅺ',
+ 'Ⅻ' => 'ⅻ',
+ 'Ⅼ' => 'ⅼ',
+ 'Ⅽ' => 'ⅽ',
+ 'Ⅾ' => 'ⅾ',
+ 'Ⅿ' => 'ⅿ',
+ 'Ↄ' => 'ↄ',
+ 'Ⓐ' => 'ⓐ',
+ 'Ⓑ' => 'ⓑ',
+ 'Ⓒ' => 'ⓒ',
+ 'Ⓓ' => 'ⓓ',
+ 'Ⓔ' => 'ⓔ',
+ 'Ⓕ' => 'ⓕ',
+ 'Ⓖ' => 'ⓖ',
+ 'Ⓗ' => 'ⓗ',
+ 'Ⓘ' => 'ⓘ',
+ 'Ⓙ' => 'ⓙ',
+ 'Ⓚ' => 'ⓚ',
+ 'Ⓛ' => 'ⓛ',
+ 'Ⓜ' => 'ⓜ',
+ 'Ⓝ' => 'ⓝ',
+ 'Ⓞ' => 'ⓞ',
+ 'Ⓟ' => 'ⓟ',
+ 'Ⓠ' => 'ⓠ',
+ 'Ⓡ' => 'ⓡ',
+ 'Ⓢ' => 'ⓢ',
+ 'Ⓣ' => 'ⓣ',
+ 'Ⓤ' => 'ⓤ',
+ 'Ⓥ' => 'ⓥ',
+ 'Ⓦ' => 'ⓦ',
+ 'Ⓧ' => 'ⓧ',
+ 'Ⓨ' => 'ⓨ',
+ 'Ⓩ' => 'ⓩ',
+ 'Ⰰ' => 'ⰰ',
+ 'Ⰱ' => 'ⰱ',
+ 'Ⰲ' => 'ⰲ',
+ 'Ⰳ' => 'ⰳ',
+ 'Ⰴ' => 'ⰴ',
+ 'Ⰵ' => 'ⰵ',
+ 'Ⰶ' => 'ⰶ',
+ 'Ⰷ' => 'ⰷ',
+ 'Ⰸ' => 'ⰸ',
+ 'Ⰹ' => 'ⰹ',
+ 'Ⰺ' => 'ⰺ',
+ 'Ⰻ' => 'ⰻ',
+ 'Ⰼ' => 'ⰼ',
+ 'Ⰽ' => 'ⰽ',
+ 'Ⰾ' => 'ⰾ',
+ 'Ⰿ' => 'ⰿ',
+ 'Ⱀ' => 'ⱀ',
+ 'Ⱁ' => 'ⱁ',
+ 'Ⱂ' => 'ⱂ',
+ 'Ⱃ' => 'ⱃ',
+ 'Ⱄ' => 'ⱄ',
+ 'Ⱅ' => 'ⱅ',
+ 'Ⱆ' => 'ⱆ',
+ 'Ⱇ' => 'ⱇ',
+ 'Ⱈ' => 'ⱈ',
+ 'Ⱉ' => 'ⱉ',
+ 'Ⱊ' => 'ⱊ',
+ 'Ⱋ' => 'ⱋ',
+ 'Ⱌ' => 'ⱌ',
+ 'Ⱍ' => 'ⱍ',
+ 'Ⱎ' => 'ⱎ',
+ 'Ⱏ' => 'ⱏ',
+ 'Ⱐ' => 'ⱐ',
+ 'Ⱑ' => 'ⱑ',
+ 'Ⱒ' => 'ⱒ',
+ 'Ⱓ' => 'ⱓ',
+ 'Ⱔ' => 'ⱔ',
+ 'Ⱕ' => 'ⱕ',
+ 'Ⱖ' => 'ⱖ',
+ 'Ⱗ' => 'ⱗ',
+ 'Ⱘ' => 'ⱘ',
+ 'Ⱙ' => 'ⱙ',
+ 'Ⱚ' => 'ⱚ',
+ 'Ⱛ' => 'ⱛ',
+ 'Ⱜ' => 'ⱜ',
+ 'Ⱝ' => 'ⱝ',
+ 'Ⱞ' => 'ⱞ',
+ 'Ⱡ' => 'ⱡ',
+ 'Ɫ' => 'ɫ',
+ 'Ᵽ' => 'ᵽ',
+ 'Ɽ' => 'ɽ',
+ 'Ⱨ' => 'ⱨ',
+ 'Ⱪ' => 'ⱪ',
+ 'Ⱬ' => 'ⱬ',
+ 'Ɑ' => 'ɑ',
+ 'Ɱ' => 'ɱ',
+ 'Ɐ' => 'ɐ',
+ 'Ⱳ' => 'ⱳ',
+ 'Ⱶ' => 'ⱶ',
+ 'Ⲁ' => 'ⲁ',
+ 'Ⲃ' => 'ⲃ',
+ 'Ⲅ' => 'ⲅ',
+ 'Ⲇ' => 'ⲇ',
+ 'Ⲉ' => 'ⲉ',
+ 'Ⲋ' => 'ⲋ',
+ 'Ⲍ' => 'ⲍ',
+ 'Ⲏ' => 'ⲏ',
+ 'Ⲑ' => 'ⲑ',
+ 'Ⲓ' => 'ⲓ',
+ 'Ⲕ' => 'ⲕ',
+ 'Ⲗ' => 'ⲗ',
+ 'Ⲙ' => 'ⲙ',
+ 'Ⲛ' => 'ⲛ',
+ 'Ⲝ' => 'ⲝ',
+ 'Ⲟ' => 'ⲟ',
+ 'Ⲡ' => 'ⲡ',
+ 'Ⲣ' => 'ⲣ',
+ 'Ⲥ' => 'ⲥ',
+ 'Ⲧ' => 'ⲧ',
+ 'Ⲩ' => 'ⲩ',
+ 'Ⲫ' => 'ⲫ',
+ 'Ⲭ' => 'ⲭ',
+ 'Ⲯ' => 'ⲯ',
+ 'Ⲱ' => 'ⲱ',
+ 'Ⲳ' => 'ⲳ',
+ 'Ⲵ' => 'ⲵ',
+ 'Ⲷ' => 'ⲷ',
+ 'Ⲹ' => 'ⲹ',
+ 'Ⲻ' => 'ⲻ',
+ 'Ⲽ' => 'ⲽ',
+ 'Ⲿ' => 'ⲿ',
+ 'Ⳁ' => 'ⳁ',
+ 'Ⳃ' => 'ⳃ',
+ 'Ⳅ' => 'ⳅ',
+ 'Ⳇ' => 'ⳇ',
+ 'Ⳉ' => 'ⳉ',
+ 'Ⳋ' => 'ⳋ',
+ 'Ⳍ' => 'ⳍ',
+ 'Ⳏ' => 'ⳏ',
+ 'Ⳑ' => 'ⳑ',
+ 'Ⳓ' => 'ⳓ',
+ 'Ⳕ' => 'ⳕ',
+ 'Ⳗ' => 'ⳗ',
+ 'Ⳙ' => 'ⳙ',
+ 'Ⳛ' => 'ⳛ',
+ 'Ⳝ' => 'ⳝ',
+ 'Ⳟ' => 'ⳟ',
+ 'Ⳡ' => 'ⳡ',
+ 'Ⳣ' => 'ⳣ',
+ 'Ꙁ' => 'ꙁ',
+ 'Ꙃ' => 'ꙃ',
+ 'Ꙅ' => 'ꙅ',
+ 'Ꙇ' => 'ꙇ',
+ 'Ꙉ' => 'ꙉ',
+ 'Ꙋ' => 'ꙋ',
+ 'Ꙍ' => 'ꙍ',
+ 'Ꙏ' => 'ꙏ',
+ 'Ꙑ' => 'ꙑ',
+ 'Ꙓ' => 'ꙓ',
+ 'Ꙕ' => 'ꙕ',
+ 'Ꙗ' => 'ꙗ',
+ 'Ꙙ' => 'ꙙ',
+ 'Ꙛ' => 'ꙛ',
+ 'Ꙝ' => 'ꙝ',
+ 'Ꙟ' => 'ꙟ',
+ 'Ꙣ' => 'ꙣ',
+ 'Ꙥ' => 'ꙥ',
+ 'Ꙧ' => 'ꙧ',
+ 'Ꙩ' => 'ꙩ',
+ 'Ꙫ' => 'ꙫ',
+ 'Ꙭ' => 'ꙭ',
+ 'Ꚁ' => 'ꚁ',
+ 'Ꚃ' => 'ꚃ',
+ 'Ꚅ' => 'ꚅ',
+ 'Ꚇ' => 'ꚇ',
+ 'Ꚉ' => 'ꚉ',
+ 'Ꚋ' => 'ꚋ',
+ 'Ꚍ' => 'ꚍ',
+ 'Ꚏ' => 'ꚏ',
+ 'Ꚑ' => 'ꚑ',
+ 'Ꚓ' => 'ꚓ',
+ 'Ꚕ' => 'ꚕ',
+ 'Ꚗ' => 'ꚗ',
+ 'Ꜣ' => 'ꜣ',
+ 'Ꜥ' => 'ꜥ',
+ 'Ꜧ' => 'ꜧ',
+ 'Ꜩ' => 'ꜩ',
+ 'Ꜫ' => 'ꜫ',
+ 'Ꜭ' => 'ꜭ',
+ 'Ꜯ' => 'ꜯ',
+ 'Ꜳ' => 'ꜳ',
+ 'Ꜵ' => 'ꜵ',
+ 'Ꜷ' => 'ꜷ',
+ 'Ꜹ' => 'ꜹ',
+ 'Ꜻ' => 'ꜻ',
+ 'Ꜽ' => 'ꜽ',
+ 'Ꜿ' => 'ꜿ',
+ 'Ꝁ' => 'ꝁ',
+ 'Ꝃ' => 'ꝃ',
+ 'Ꝅ' => 'ꝅ',
+ 'Ꝇ' => 'ꝇ',
+ 'Ꝉ' => 'ꝉ',
+ 'Ꝋ' => 'ꝋ',
+ 'Ꝍ' => 'ꝍ',
+ 'Ꝏ' => 'ꝏ',
+ 'Ꝑ' => 'ꝑ',
+ 'Ꝓ' => 'ꝓ',
+ 'Ꝕ' => 'ꝕ',
+ 'Ꝗ' => 'ꝗ',
+ 'Ꝙ' => 'ꝙ',
+ 'Ꝛ' => 'ꝛ',
+ 'Ꝝ' => 'ꝝ',
+ 'Ꝟ' => 'ꝟ',
+ 'Ꝡ' => 'ꝡ',
+ 'Ꝣ' => 'ꝣ',
+ 'Ꝥ' => 'ꝥ',
+ 'Ꝧ' => 'ꝧ',
+ 'Ꝩ' => 'ꝩ',
+ 'Ꝫ' => 'ꝫ',
+ 'Ꝭ' => 'ꝭ',
+ 'Ꝯ' => 'ꝯ',
+ 'Ꝺ' => 'ꝺ',
+ 'Ꝼ' => 'ꝼ',
+ 'Ᵹ' => 'ᵹ',
+ 'Ꝿ' => 'ꝿ',
+ 'Ꞁ' => 'ꞁ',
+ 'Ꞃ' => 'ꞃ',
+ 'Ꞅ' => 'ꞅ',
+ 'Ꞇ' => 'ꞇ',
+ 'Ꞌ' => 'ꞌ',
+ 'A' => 'a',
+ 'B' => 'b',
+ 'C' => 'c',
+ 'D' => 'd',
+ 'E' => 'e',
+ 'F' => 'f',
+ 'G' => 'g',
+ 'H' => 'h',
+ 'I' => 'i',
+ 'J' => 'j',
+ 'K' => 'k',
+ 'L' => 'l',
+ 'M' => 'm',
+ 'N' => 'n',
+ 'O' => 'o',
+ 'P' => 'p',
+ 'Q' => 'q',
+ 'R' => 'r',
+ 'S' => 's',
+ 'T' => 't',
+ 'U' => 'u',
+ 'V' => 'v',
+ 'W' => 'w',
+ 'X' => 'x',
+ 'Y' => 'y',
+ 'Z' => 'z',
+ '𐐀' => '𐐨',
+ '𐐁' => '𐐩',
+ '𐐂' => '𐐪',
+ '𐐃' => '𐐫',
+ '𐐄' => '𐐬',
+ '𐐅' => '𐐭',
+ '𐐆' => '𐐮',
+ '𐐇' => '𐐯',
+ '𐐈' => '𐐰',
+ '𐐉' => '𐐱',
+ '𐐊' => '𐐲',
+ '𐐋' => '𐐳',
+ '𐐌' => '𐐴',
+ '𐐍' => '𐐵',
+ '𐐎' => '𐐶',
+ '𐐏' => '𐐷',
+ '𐐐' => '𐐸',
+ '𐐑' => '𐐹',
+ '𐐒' => '𐐺',
+ '𐐓' => '𐐻',
+ '𐐔' => '𐐼',
+ '𐐕' => '𐐽',
+ '𐐖' => '𐐾',
+ '𐐗' => '𐐿',
+ '𐐘' => '𐑀',
+ '𐐙' => '𐑁',
+ '𐐚' => '𐑂',
+ '𐐛' => '𐑃',
+ '𐐜' => '𐑄',
+ '𐐝' => '𐑅',
+ '𐐞' => '𐑆',
+ '𐐟' => '𐑇',
+ '𐐠' => '𐑈',
+ '𐐡' => '𐑉',
+ '𐐢' => '𐑊',
+ '𐐣' => '𐑋',
+ '𐐤' => '𐑌',
+ '𐐥' => '𐑍',
+ '𐐦' => '𐑎',
+ '𐐧' => '𐑏'
+);
diff --git a/includes/normal/Utf8CaseGenerate.php b/includes/normal/Utf8CaseGenerate.php
new file mode 100644
index 00000000..8dbbb72a
--- /dev/null
+++ b/includes/normal/Utf8CaseGenerate.php
@@ -0,0 +1,112 @@
+<?php
+# Copyright (C) 2004,2008 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
+
+/**
+ * This script generates Utf8Case.inc from the Unicode Character Database
+ * and supplementary files.
+ *
+ * @ingroup UtfNormal
+ * @access private
+ */
+
+/** */
+
+if( php_sapi_name() != 'cli' ) {
+ die( "Run me from the command line please.\n" );
+}
+
+require_once 'UtfNormalUtil.php';
+
+$in = fopen("UnicodeData.txt", "rt" );
+if( !$in ) {
+ print "Can't open UnicodeData.txt for reading.\n";
+ print "If necessary, fetch this file from the internet:\n";
+ print "http://www.unicode.org/Public/UNIDATA/UnicodeData.txt\n";
+ exit(-1);
+}
+$wikiUpperChars = array();
+$wikiLowerChars = array();
+
+print "Reading character definitions...\n";
+while( false !== ($line = fgets( $in ) ) ) {
+ $columns = split(';', $line);
+ $codepoint = $columns[0];
+ $name = $columns[1];
+ $simpleUpper = $columns[12];
+ $simpleLower = $columns[13];
+
+ $source = codepointToUtf8( hexdec( $codepoint ) );
+ if( $simpleUpper ) {
+ $wikiUpperChars[$source] = codepointToUtf8( hexdec( $simpleUpper ) );
+ }
+ if( $simpleLower ) {
+ $wikiLowerChars[$source] = codepointToUtf8( hexdec( $simpleLower ) );
+ }
+}
+fclose( $in );
+
+$out = fopen("Utf8Case.php", "wt");
+if( $out ) {
+ $outUpperChars = escapeArray( $wikiUpperChars );
+ $outLowerChars = escapeArray( $wikiLowerChars );
+ $outdata = "<" . "?php
+/**
+ * Simple 1:1 upper/lowercase switching arrays for utf-8 text
+ * Won't get context-sensitive things yet
+ *
+ * Hack for bugs in ucfirst() and company
+ *
+ * These are pulled from memcached if possible, as this is faster than filling
+ * up a big array manually.
+ * @ingroup Language
+ */
+
+/*
+ * Translation array to get upper case character
+ */
+
+\$wikiUpperChars = $outUpperChars;
+
+/*
+ * Translation array to get lower case character
+ */
+\$wikiLowerChars = $outLowerChars;\n";
+ fputs( $out, $outdata );
+ fclose( $out );
+ print "Wrote out Utf8Case.php\n";
+} else {
+ print "Can't create file Utf8Case.php\n";
+ exit(-1);
+}
+
+
+function escapeArray( $arr ) {
+ return "array(\n" .
+ implode( ",\n",
+ array_map( "escapeLine",
+ array_keys( $arr ),
+ array_values( $arr ) ) ) .
+ "\n)";
+}
+
+function escapeLine( $key, $val ) {
+ $encKey = escapeSingleString( $key );
+ $encVal = escapeSingleString( $val );
+ return "\t'$encKey' => '$encVal'";
+}
diff --git a/includes/normal/Utf8Test.php b/includes/normal/Utf8Test.php
index 8600d49d..353d11b5 100644
--- a/includes/normal/Utf8Test.php
+++ b/includes/normal/Utf8Test.php
@@ -21,7 +21,7 @@
* Runs the UTF-8 decoder test at:
* http://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt
*
- * @addtogroup UtfNormal
+ * @ingroup UtfNormal
* @access private
*/
@@ -149,5 +149,3 @@ function testLine( $test, $line, &$total, &$success, &$failed ) {
print str_replace( "\n", "$len\n", $stripped );
}
}
-
-
diff --git a/includes/normal/UtfNormal.php b/includes/normal/UtfNormal.php
index 557b8e5e..4f8b1293 100644
--- a/includes/normal/UtfNormal.php
+++ b/includes/normal/UtfNormal.php
@@ -17,6 +17,10 @@
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# http://www.gnu.org/copyleft/gpl.html
+/**
+ * @defgroup UtfNormal UtfNormal
+ */
+
/** */
require_once dirname(__FILE__).'/UtfNormalUtil.php';
@@ -54,7 +58,7 @@ define( 'NORMALIZE_ICU', function_exists( 'utf8_normalize' ) );
*
* See description of forms at http://www.unicode.org/reports/tr15/
*
- * @addtogroup UtfNormal
+ * @ingroup UtfNormal
*/
class UtfNormal {
/**
@@ -64,9 +68,8 @@ class UtfNormal {
* Fast return for pure ASCII strings; some lesser optimizations for
* strings containing only known-good characters. Not as fast as toNFC().
*
- * @param string $string a UTF-8 string
+ * @param $string String: a UTF-8 string
* @return string a clean, shiny, normalized UTF-8 string
- * @static
*/
static function cleanUp( $string ) {
if( NORMALIZE_ICU ) {
@@ -94,9 +97,8 @@ class UtfNormal {
* Fast return for pure ASCII strings; some lesser optimizations for
* strings containing only known-good characters.
*
- * @param string $string a valid UTF-8 string. Input is not validated.
+ * @param $string String: a valid UTF-8 string. Input is not validated.
* @return string a UTF-8 string in normal form C
- * @static
*/
static function toNFC( $string ) {
if( NORMALIZE_ICU )
@@ -111,9 +113,8 @@ class UtfNormal {
* Convert a UTF-8 string to normal form D, canonical decomposition.
* Fast return for pure ASCII strings.
*
- * @param string $string a valid UTF-8 string. Input is not validated.
+ * @param $string String: a valid UTF-8 string. Input is not validated.
* @return string a UTF-8 string in normal form D
- * @static
*/
static function toNFD( $string ) {
if( NORMALIZE_ICU )
@@ -129,9 +130,8 @@ class UtfNormal {
* This may cause irreversible information loss, use judiciously.
* Fast return for pure ASCII strings.
*
- * @param string $string a valid UTF-8 string. Input is not validated.
+ * @param $string String: a valid UTF-8 string. Input is not validated.
* @return string a UTF-8 string in normal form KC
- * @static
*/
static function toNFKC( $string ) {
if( NORMALIZE_ICU )
@@ -147,9 +147,8 @@ class UtfNormal {
* This may cause irreversible information loss, use judiciously.
* Fast return for pure ASCII strings.
*
- * @param string $string a valid UTF-8 string. Input is not validated.
+ * @param $string String: a valid UTF-8 string. Input is not validated.
* @return string a UTF-8 string in normal form KD
- * @static
*/
static function toNFKD( $string ) {
if( NORMALIZE_ICU )
@@ -163,7 +162,6 @@ class UtfNormal {
/**
* Load the basic composition data if necessary
* @private
- * @static
*/
static function loadData() {
global $utfCombiningClass;
@@ -175,9 +173,8 @@ class UtfNormal {
/**
* Returns true if the string is _definitely_ in NFC.
* Returns false if not or uncertain.
- * @param string $string a valid UTF-8 string. Input is not validated.
+ * @param $string String: a valid UTF-8 string. Input is not validated.
* @return bool
- * @static
*/
static function quickIsNFC( $string ) {
# ASCII is always valid NFC!
@@ -217,8 +214,7 @@ class UtfNormal {
/**
* Returns true if the string is _definitely_ in NFC.
* Returns false if not or uncertain.
- * @param string $string a UTF-8 string, altered on output to be valid UTF-8 safe for XML.
- * @static
+ * @param $string String: a UTF-8 string, altered on output to be valid UTF-8 safe for XML.
*/
static function quickIsNFCVerify( &$string ) {
# Screen out some characters that eg won't be allowed in XML
@@ -435,20 +431,18 @@ class UtfNormal {
# checking for validity or any optimization etc. Input must be
# VALID UTF-8!
/**
- * @param string $string
+ * @param $string string
* @return string
* @private
- * @static
*/
static function NFC( $string ) {
return UtfNormal::fastCompose( UtfNormal::NFD( $string ) );
}
/**
- * @param string $string
+ * @param $string string
* @return string
* @private
- * @static
*/
static function NFD( $string ) {
UtfNormal::loadData();
@@ -458,20 +452,18 @@ class UtfNormal {
}
/**
- * @param string $string
+ * @param $string string
* @return string
* @private
- * @static
*/
static function NFKC( $string ) {
return UtfNormal::fastCompose( UtfNormal::NFKD( $string ) );
}
/**
- * @param string $string
+ * @param $string string
* @return string
* @private
- * @static
*/
static function NFKD( $string ) {
global $utfCompatibilityDecomp;
@@ -488,10 +480,9 @@ class UtfNormal {
* (depending on which decomposition map is passed to us).
* Input is assumed to be *valid* UTF-8. Invalid code will break.
* @private
- * @param string $string Valid UTF-8 string
- * @param array $map hash of expanded decomposition map
+ * @param $string String: valid UTF-8 string
+ * @param $map Array: hash of expanded decomposition map
* @return string a UTF-8 string decomposed, not yet normalized (needs sorting)
- * @static
*/
static function fastDecompose( $string, $map ) {
UtfNormal::loadData();
@@ -550,9 +541,8 @@ class UtfNormal {
* Sorts combining characters into canonical order. This is the
* final step in creating decomposed normal forms D and KD.
* @private
- * @param string $string a valid, decomposed UTF-8 string. Input is not validated.
+ * @param $string String: a valid, decomposed UTF-8 string. Input is not validated.
* @return string a UTF-8 string with combining characters sorted in canonical order
- * @static
*/
static function fastCombiningSort( $string ) {
UtfNormal::loadData();
@@ -604,9 +594,8 @@ class UtfNormal {
* Produces canonically composed sequences, i.e. normal form C or KC.
*
* @private
- * @param string $string a valid UTF-8 string in sorted normal form D or KD. Input is not validated.
+ * @param $string String: a valid UTF-8 string in sorted normal form D or KD. Input is not validated.
* @return string a UTF-8 string with canonical precomposed characters used where possible
- * @static
*/
static function fastCompose( $string ) {
UtfNormal::loadData();
@@ -737,9 +726,8 @@ class UtfNormal {
/**
* This is just used for the benchmark, comparing how long it takes to
* interate through a string without really doing anything of substance.
- * @param string $string
+ * @param $string string
* @return string
- * @static
*/
static function placebo( $string ) {
$len = strlen( $string );
@@ -750,5 +738,3 @@ class UtfNormal {
return $out;
}
}
-
-
diff --git a/includes/normal/UtfNormalBench.php b/includes/normal/UtfNormalBench.php
index d89b0eb5..1bcd8334 100644
--- a/includes/normal/UtfNormalBench.php
+++ b/includes/normal/UtfNormalBench.php
@@ -20,7 +20,7 @@
/**
* Approximate benchmark for some basic operations.
*
- * @addtogroup UtfNormal
+ * @ingroup UtfNormal
* @access private
*/
@@ -107,5 +107,3 @@ function benchmarkForm( &$u, &$data, $form ) {
($same ? 'no change' : 'changed' ) );
return $out;
}
-
-
diff --git a/includes/normal/UtfNormalData.inc b/includes/normal/UtfNormalData.inc
index 91c15769..cf942f68 100644
--- a/includes/normal/UtfNormalData.inc
+++ b/includes/normal/UtfNormalData.inc
@@ -5,7 +5,7 @@
*/
/** */
global $utfCombiningClass, $utfCanonicalComp, $utfCanonicalDecomp, $utfCheckNFC;
-$utfCombiningClass = unserialize( 'a:418:{s:2:"̀";i:230;s:2:"́";i:230;s:2:"̂";i:230;s:2:"̃";i:230;s:2:"̄";i:230;s:2:"̅";i:230;s:2:"̆";i:230;s:2:"̇";i:230;s:2:"̈";i:230;s:2:"̉";i:230;s:2:"̊";i:230;s:2:"̋";i:230;s:2:"̌";i:230;s:2:"̍";i:230;s:2:"̎";i:230;s:2:"̏";i:230;s:2:"̐";i:230;s:2:"̑";i:230;s:2:"̒";i:230;s:2:"̓";i:230;s:2:"̔";i:230;s:2:"̕";i:232;s:2:"̖";i:220;s:2:"̗";i:220;s:2:"̘";i:220;s:2:"̙";i:220;s:2:"̚";i:232;s:2:"̛";i:216;s:2:"̜";i:220;s:2:"̝";i:220;s:2:"̞";i:220;s:2:"̟";i:220;s:2:"̠";i:220;s:2:"̡";i:202;s:2:"̢";i:202;s:2:"̣";i:220;s:2:"̤";i:220;s:2:"̥";i:220;s:2:"̦";i:220;s:2:"̧";i:202;s:2:"̨";i:202;s:2:"̩";i:220;s:2:"̪";i:220;s:2:"̫";i:220;s:2:"̬";i:220;s:2:"̭";i:220;s:2:"̮";i:220;s:2:"̯";i:220;s:2:"̰";i:220;s:2:"̱";i:220;s:2:"̲";i:220;s:2:"̳";i:220;s:2:"̴";i:1;s:2:"̵";i:1;s:2:"̶";i:1;s:2:"̷";i:1;s:2:"̸";i:1;s:2:"̹";i:220;s:2:"̺";i:220;s:2:"̻";i:220;s:2:"̼";i:220;s:2:"̽";i:230;s:2:"̾";i:230;s:2:"̿";i:230;s:2:"̀";i:230;s:2:"́";i:230;s:2:"͂";i:230;s:2:"̓";i:230;s:2:"̈́";i:230;s:2:"ͅ";i:240;s:2:"͆";i:230;s:2:"͇";i:220;s:2:"͈";i:220;s:2:"͉";i:220;s:2:"͊";i:230;s:2:"͋";i:230;s:2:"͌";i:230;s:2:"͍";i:220;s:2:"͎";i:220;s:2:"͐";i:230;s:2:"͑";i:230;s:2:"͒";i:230;s:2:"͓";i:220;s:2:"͔";i:220;s:2:"͕";i:220;s:2:"͖";i:220;s:2:"͗";i:230;s:2:"͘";i:232;s:2:"͙";i:220;s:2:"͚";i:220;s:2:"͛";i:230;s:2:"͜";i:233;s:2:"͝";i:234;s:2:"͞";i:234;s:2:"͟";i:233;s:2:"͠";i:234;s:2:"͡";i:234;s:2:"͢";i:233;s:2:"ͣ";i:230;s:2:"ͤ";i:230;s:2:"ͥ";i:230;s:2:"ͦ";i:230;s:2:"ͧ";i:230;s:2:"ͨ";i:230;s:2:"ͩ";i:230;s:2:"ͪ";i:230;s:2:"ͫ";i:230;s:2:"ͬ";i:230;s:2:"ͭ";i:230;s:2:"ͮ";i:230;s:2:"ͯ";i:230;s:2:"҃";i:230;s:2:"҄";i:230;s:2:"҅";i:230;s:2:"҆";i:230;s:2:"֑";i:220;s:2:"֒";i:230;s:2:"֓";i:230;s:2:"֔";i:230;s:2:"֕";i:230;s:2:"֖";i:220;s:2:"֗";i:230;s:2:"֘";i:230;s:2:"֙";i:230;s:2:"֚";i:222;s:2:"֛";i:220;s:2:"֜";i:230;s:2:"֝";i:230;s:2:"֞";i:230;s:2:"֟";i:230;s:2:"֠";i:230;s:2:"֡";i:230;s:2:"֢";i:220;s:2:"֣";i:220;s:2:"֤";i:220;s:2:"֥";i:220;s:2:"֦";i:220;s:2:"֧";i:220;s:2:"֨";i:230;s:2:"֩";i:230;s:2:"֪";i:220;s:2:"֫";i:230;s:2:"֬";i:230;s:2:"֭";i:222;s:2:"֮";i:228;s:2:"֯";i:230;s:2:"ְ";i:10;s:2:"ֱ";i:11;s:2:"ֲ";i:12;s:2:"ֳ";i:13;s:2:"ִ";i:14;s:2:"ֵ";i:15;s:2:"ֶ";i:16;s:2:"ַ";i:17;s:2:"ָ";i:18;s:2:"ֹ";i:19;s:2:"ֺ";i:19;s:2:"ֻ";i:20;s:2:"ּ";i:21;s:2:"ֽ";i:22;s:2:"ֿ";i:23;s:2:"ׁ";i:24;s:2:"ׂ";i:25;s:2:"ׄ";i:230;s:2:"ׅ";i:220;s:2:"ׇ";i:18;s:2:"ؐ";i:230;s:2:"ؑ";i:230;s:2:"ؒ";i:230;s:2:"ؓ";i:230;s:2:"ؔ";i:230;s:2:"ؕ";i:230;s:2:"ً";i:27;s:2:"ٌ";i:28;s:2:"ٍ";i:29;s:2:"َ";i:30;s:2:"ُ";i:31;s:2:"ِ";i:32;s:2:"ّ";i:33;s:2:"ْ";i:34;s:2:"ٓ";i:230;s:2:"ٔ";i:230;s:2:"ٕ";i:220;s:2:"ٖ";i:220;s:2:"ٗ";i:230;s:2:"٘";i:230;s:2:"ٙ";i:230;s:2:"ٚ";i:230;s:2:"ٛ";i:230;s:2:"ٜ";i:220;s:2:"ٝ";i:230;s:2:"ٞ";i:230;s:2:"ٰ";i:35;s:2:"ۖ";i:230;s:2:"ۗ";i:230;s:2:"ۘ";i:230;s:2:"ۙ";i:230;s:2:"ۚ";i:230;s:2:"ۛ";i:230;s:2:"ۜ";i:230;s:2:"۟";i:230;s:2:"۠";i:230;s:2:"ۡ";i:230;s:2:"ۢ";i:230;s:2:"ۣ";i:220;s:2:"ۤ";i:230;s:2:"ۧ";i:230;s:2:"ۨ";i:230;s:2:"۪";i:220;s:2:"۫";i:230;s:2:"۬";i:230;s:2:"ۭ";i:220;s:2:"ܑ";i:36;s:2:"ܰ";i:230;s:2:"ܱ";i:220;s:2:"ܲ";i:230;s:2:"ܳ";i:230;s:2:"ܴ";i:220;s:2:"ܵ";i:230;s:2:"ܶ";i:230;s:2:"ܷ";i:220;s:2:"ܸ";i:220;s:2:"ܹ";i:220;s:2:"ܺ";i:230;s:2:"ܻ";i:220;s:2:"ܼ";i:220;s:2:"ܽ";i:230;s:2:"ܾ";i:220;s:2:"ܿ";i:230;s:2:"݀";i:230;s:2:"݁";i:230;s:2:"݂";i:220;s:2:"݃";i:230;s:2:"݄";i:220;s:2:"݅";i:230;s:2:"݆";i:220;s:2:"݇";i:230;s:2:"݈";i:220;s:2:"݉";i:230;s:2:"݊";i:230;s:2:"߫";i:230;s:2:"߬";i:230;s:2:"߭";i:230;s:2:"߮";i:230;s:2:"߯";i:230;s:2:"߰";i:230;s:2:"߱";i:230;s:2:"߲";i:220;s:2:"߳";i:230;s:3:"़";i:7;s:3:"्";i:9;s:3:"॑";i:230;s:3:"॒";i:220;s:3:"॓";i:230;s:3:"॔";i:230;s:3:"়";i:7;s:3:"্";i:9;s:3:"਼";i:7;s:3:"੍";i:9;s:3:"઼";i:7;s:3:"્";i:9;s:3:"଼";i:7;s:3:"୍";i:9;s:3:"்";i:9;s:3:"్";i:9;s:3:"ౕ";i:84;s:3:"ౖ";i:91;s:3:"಼";i:7;s:3:"್";i:9;s:3:"്";i:9;s:3:"්";i:9;s:3:"ุ";i:103;s:3:"ู";i:103;s:3:"ฺ";i:9;s:3:"่";i:107;s:3:"้";i:107;s:3:"๊";i:107;s:3:"๋";i:107;s:3:"ຸ";i:118;s:3:"ູ";i:118;s:3:"່";i:122;s:3:"້";i:122;s:3:"໊";i:122;s:3:"໋";i:122;s:3:"༘";i:220;s:3:"༙";i:220;s:3:"༵";i:220;s:3:"༷";i:220;s:3:"༹";i:216;s:3:"ཱ";i:129;s:3:"ི";i:130;s:3:"ུ";i:132;s:3:"ེ";i:130;s:3:"ཻ";i:130;s:3:"ོ";i:130;s:3:"ཽ";i:130;s:3:"ྀ";i:130;s:3:"ྂ";i:230;s:3:"ྃ";i:230;s:3:"྄";i:9;s:3:"྆";i:230;s:3:"྇";i:230;s:3:"࿆";i:220;s:3:"့";i:7;s:3:"္";i:9;s:3:"፟";i:230;s:3:"᜔";i:9;s:3:"᜴";i:9;s:3:"្";i:9;s:3:"៝";i:230;s:3:"ᢩ";i:228;s:3:"᤹";i:222;s:3:"᤺";i:230;s:3:"᤻";i:220;s:3:"ᨗ";i:230;s:3:"ᨘ";i:220;s:3:"᬴";i:7;s:3:"᭄";i:9;s:3:"᭫";i:230;s:3:"᭬";i:220;s:3:"᭭";i:230;s:3:"᭮";i:230;s:3:"᭯";i:230;s:3:"᭰";i:230;s:3:"᭱";i:230;s:3:"᭲";i:230;s:3:"᭳";i:230;s:3:"᷀";i:230;s:3:"᷁";i:230;s:3:"᷂";i:220;s:3:"᷃";i:230;s:3:"᷄";i:230;s:3:"᷅";i:230;s:3:"᷆";i:230;s:3:"᷇";i:230;s:3:"᷈";i:230;s:3:"᷉";i:230;s:3:"᷊";i:220;s:3:"᷾";i:230;s:3:"᷿";i:220;s:3:"⃐";i:230;s:3:"⃑";i:230;s:3:"⃒";i:1;s:3:"⃓";i:1;s:3:"⃔";i:230;s:3:"⃕";i:230;s:3:"⃖";i:230;s:3:"⃗";i:230;s:3:"⃘";i:1;s:3:"⃙";i:1;s:3:"⃚";i:1;s:3:"⃛";i:230;s:3:"⃜";i:230;s:3:"⃡";i:230;s:3:"⃥";i:1;s:3:"⃦";i:1;s:3:"⃧";i:230;s:3:"⃨";i:220;s:3:"⃩";i:230;s:3:"⃪";i:1;s:3:"⃫";i:1;s:3:"⃬";i:220;s:3:"⃭";i:220;s:3:"⃮";i:220;s:3:"⃯";i:220;s:3:"〪";i:218;s:3:"〫";i:228;s:3:"〬";i:232;s:3:"〭";i:222;s:3:"〮";i:224;s:3:"〯";i:224;s:3:"゙";i:8;s:3:"゚";i:8;s:3:"꠆";i:9;s:3:"ﬞ";i:26;s:3:"︠";i:230;s:3:"︡";i:230;s:3:"︢";i:230;s:3:"︣";i:230;s:4:"𐨍";i:220;s:4:"𐨏";i:230;s:4:"𐨸";i:230;s:4:"𐨹";i:1;s:4:"𐨺";i:220;s:4:"𐨿";i:9;s:4:"𝅥";i:216;s:4:"𝅦";i:216;s:4:"𝅧";i:1;s:4:"𝅨";i:1;s:4:"𝅩";i:1;s:4:"𝅭";i:226;s:4:"𝅮";i:216;s:4:"𝅯";i:216;s:4:"𝅰";i:216;s:4:"𝅱";i:216;s:4:"𝅲";i:216;s:4:"𝅻";i:220;s:4:"𝅼";i:220;s:4:"𝅽";i:220;s:4:"𝅾";i:220;s:4:"𝅿";i:220;s:4:"𝆀";i:220;s:4:"𝆁";i:220;s:4:"𝆂";i:220;s:4:"𝆅";i:230;s:4:"𝆆";i:230;s:4:"𝆇";i:230;s:4:"𝆈";i:230;s:4:"𝆉";i:230;s:4:"𝆊";i:220;s:4:"𝆋";i:220;s:4:"𝆪";i:230;s:4:"𝆫";i:230;s:4:"𝆬";i:230;s:4:"𝆭";i:230;s:4:"𝉂";i:230;s:4:"𝉃";i:230;s:4:"𝉄";i:230;}' );
+$utfCombiningClass = unserialize( 'a:501:{s:2:"̀";i:230;s:2:"́";i:230;s:2:"̂";i:230;s:2:"̃";i:230;s:2:"̄";i:230;s:2:"̅";i:230;s:2:"̆";i:230;s:2:"̇";i:230;s:2:"̈";i:230;s:2:"̉";i:230;s:2:"̊";i:230;s:2:"̋";i:230;s:2:"̌";i:230;s:2:"̍";i:230;s:2:"̎";i:230;s:2:"̏";i:230;s:2:"̐";i:230;s:2:"̑";i:230;s:2:"̒";i:230;s:2:"̓";i:230;s:2:"̔";i:230;s:2:"̕";i:232;s:2:"̖";i:220;s:2:"̗";i:220;s:2:"̘";i:220;s:2:"̙";i:220;s:2:"̚";i:232;s:2:"̛";i:216;s:2:"̜";i:220;s:2:"̝";i:220;s:2:"̞";i:220;s:2:"̟";i:220;s:2:"̠";i:220;s:2:"̡";i:202;s:2:"̢";i:202;s:2:"̣";i:220;s:2:"̤";i:220;s:2:"̥";i:220;s:2:"̦";i:220;s:2:"̧";i:202;s:2:"̨";i:202;s:2:"̩";i:220;s:2:"̪";i:220;s:2:"̫";i:220;s:2:"̬";i:220;s:2:"̭";i:220;s:2:"̮";i:220;s:2:"̯";i:220;s:2:"̰";i:220;s:2:"̱";i:220;s:2:"̲";i:220;s:2:"̳";i:220;s:2:"̴";i:1;s:2:"̵";i:1;s:2:"̶";i:1;s:2:"̷";i:1;s:2:"̸";i:1;s:2:"̹";i:220;s:2:"̺";i:220;s:2:"̻";i:220;s:2:"̼";i:220;s:2:"̽";i:230;s:2:"̾";i:230;s:2:"̿";i:230;s:2:"̀";i:230;s:2:"́";i:230;s:2:"͂";i:230;s:2:"̓";i:230;s:2:"̈́";i:230;s:2:"ͅ";i:240;s:2:"͆";i:230;s:2:"͇";i:220;s:2:"͈";i:220;s:2:"͉";i:220;s:2:"͊";i:230;s:2:"͋";i:230;s:2:"͌";i:230;s:2:"͍";i:220;s:2:"͎";i:220;s:2:"͐";i:230;s:2:"͑";i:230;s:2:"͒";i:230;s:2:"͓";i:220;s:2:"͔";i:220;s:2:"͕";i:220;s:2:"͖";i:220;s:2:"͗";i:230;s:2:"͘";i:232;s:2:"͙";i:220;s:2:"͚";i:220;s:2:"͛";i:230;s:2:"͜";i:233;s:2:"͝";i:234;s:2:"͞";i:234;s:2:"͟";i:233;s:2:"͠";i:234;s:2:"͡";i:234;s:2:"͢";i:233;s:2:"ͣ";i:230;s:2:"ͤ";i:230;s:2:"ͥ";i:230;s:2:"ͦ";i:230;s:2:"ͧ";i:230;s:2:"ͨ";i:230;s:2:"ͩ";i:230;s:2:"ͪ";i:230;s:2:"ͫ";i:230;s:2:"ͬ";i:230;s:2:"ͭ";i:230;s:2:"ͮ";i:230;s:2:"ͯ";i:230;s:2:"҃";i:230;s:2:"҄";i:230;s:2:"҅";i:230;s:2:"҆";i:230;s:2:"҇";i:230;s:2:"֑";i:220;s:2:"֒";i:230;s:2:"֓";i:230;s:2:"֔";i:230;s:2:"֕";i:230;s:2:"֖";i:220;s:2:"֗";i:230;s:2:"֘";i:230;s:2:"֙";i:230;s:2:"֚";i:222;s:2:"֛";i:220;s:2:"֜";i:230;s:2:"֝";i:230;s:2:"֞";i:230;s:2:"֟";i:230;s:2:"֠";i:230;s:2:"֡";i:230;s:2:"֢";i:220;s:2:"֣";i:220;s:2:"֤";i:220;s:2:"֥";i:220;s:2:"֦";i:220;s:2:"֧";i:220;s:2:"֨";i:230;s:2:"֩";i:230;s:2:"֪";i:220;s:2:"֫";i:230;s:2:"֬";i:230;s:2:"֭";i:222;s:2:"֮";i:228;s:2:"֯";i:230;s:2:"ְ";i:10;s:2:"ֱ";i:11;s:2:"ֲ";i:12;s:2:"ֳ";i:13;s:2:"ִ";i:14;s:2:"ֵ";i:15;s:2:"ֶ";i:16;s:2:"ַ";i:17;s:2:"ָ";i:18;s:2:"ֹ";i:19;s:2:"ֺ";i:19;s:2:"ֻ";i:20;s:2:"ּ";i:21;s:2:"ֽ";i:22;s:2:"ֿ";i:23;s:2:"ׁ";i:24;s:2:"ׂ";i:25;s:2:"ׄ";i:230;s:2:"ׅ";i:220;s:2:"ׇ";i:18;s:2:"ؐ";i:230;s:2:"ؑ";i:230;s:2:"ؒ";i:230;s:2:"ؓ";i:230;s:2:"ؔ";i:230;s:2:"ؕ";i:230;s:2:"ؖ";i:230;s:2:"ؗ";i:230;s:2:"ؘ";i:30;s:2:"ؙ";i:31;s:2:"ؚ";i:32;s:2:"ً";i:27;s:2:"ٌ";i:28;s:2:"ٍ";i:29;s:2:"َ";i:30;s:2:"ُ";i:31;s:2:"ِ";i:32;s:2:"ّ";i:33;s:2:"ْ";i:34;s:2:"ٓ";i:230;s:2:"ٔ";i:230;s:2:"ٕ";i:220;s:2:"ٖ";i:220;s:2:"ٗ";i:230;s:2:"٘";i:230;s:2:"ٙ";i:230;s:2:"ٚ";i:230;s:2:"ٛ";i:230;s:2:"ٜ";i:220;s:2:"ٝ";i:230;s:2:"ٞ";i:230;s:2:"ٰ";i:35;s:2:"ۖ";i:230;s:2:"ۗ";i:230;s:2:"ۘ";i:230;s:2:"ۙ";i:230;s:2:"ۚ";i:230;s:2:"ۛ";i:230;s:2:"ۜ";i:230;s:2:"۟";i:230;s:2:"۠";i:230;s:2:"ۡ";i:230;s:2:"ۢ";i:230;s:2:"ۣ";i:220;s:2:"ۤ";i:230;s:2:"ۧ";i:230;s:2:"ۨ";i:230;s:2:"۪";i:220;s:2:"۫";i:230;s:2:"۬";i:230;s:2:"ۭ";i:220;s:2:"ܑ";i:36;s:2:"ܰ";i:230;s:2:"ܱ";i:220;s:2:"ܲ";i:230;s:2:"ܳ";i:230;s:2:"ܴ";i:220;s:2:"ܵ";i:230;s:2:"ܶ";i:230;s:2:"ܷ";i:220;s:2:"ܸ";i:220;s:2:"ܹ";i:220;s:2:"ܺ";i:230;s:2:"ܻ";i:220;s:2:"ܼ";i:220;s:2:"ܽ";i:230;s:2:"ܾ";i:220;s:2:"ܿ";i:230;s:2:"݀";i:230;s:2:"݁";i:230;s:2:"݂";i:220;s:2:"݃";i:230;s:2:"݄";i:220;s:2:"݅";i:230;s:2:"݆";i:220;s:2:"݇";i:230;s:2:"݈";i:220;s:2:"݉";i:230;s:2:"݊";i:230;s:2:"߫";i:230;s:2:"߬";i:230;s:2:"߭";i:230;s:2:"߮";i:230;s:2:"߯";i:230;s:2:"߰";i:230;s:2:"߱";i:230;s:2:"߲";i:220;s:2:"߳";i:230;s:3:"़";i:7;s:3:"्";i:9;s:3:"॑";i:230;s:3:"॒";i:220;s:3:"॓";i:230;s:3:"॔";i:230;s:3:"়";i:7;s:3:"্";i:9;s:3:"਼";i:7;s:3:"੍";i:9;s:3:"઼";i:7;s:3:"્";i:9;s:3:"଼";i:7;s:3:"୍";i:9;s:3:"்";i:9;s:3:"్";i:9;s:3:"ౕ";i:84;s:3:"ౖ";i:91;s:3:"಼";i:7;s:3:"್";i:9;s:3:"്";i:9;s:3:"්";i:9;s:3:"ุ";i:103;s:3:"ู";i:103;s:3:"ฺ";i:9;s:3:"่";i:107;s:3:"้";i:107;s:3:"๊";i:107;s:3:"๋";i:107;s:3:"ຸ";i:118;s:3:"ູ";i:118;s:3:"່";i:122;s:3:"້";i:122;s:3:"໊";i:122;s:3:"໋";i:122;s:3:"༘";i:220;s:3:"༙";i:220;s:3:"༵";i:220;s:3:"༷";i:220;s:3:"༹";i:216;s:3:"ཱ";i:129;s:3:"ི";i:130;s:3:"ུ";i:132;s:3:"ེ";i:130;s:3:"ཻ";i:130;s:3:"ོ";i:130;s:3:"ཽ";i:130;s:3:"ྀ";i:130;s:3:"ྂ";i:230;s:3:"ྃ";i:230;s:3:"྄";i:9;s:3:"྆";i:230;s:3:"྇";i:230;s:3:"࿆";i:220;s:3:"့";i:7;s:3:"္";i:9;s:3:"်";i:9;s:3:"ႍ";i:220;s:3:"፟";i:230;s:3:"᜔";i:9;s:3:"᜴";i:9;s:3:"្";i:9;s:3:"៝";i:230;s:3:"ᢩ";i:228;s:3:"᤹";i:222;s:3:"᤺";i:230;s:3:"᤻";i:220;s:3:"ᨗ";i:230;s:3:"ᨘ";i:220;s:3:"᬴";i:7;s:3:"᭄";i:9;s:3:"᭫";i:230;s:3:"᭬";i:220;s:3:"᭭";i:230;s:3:"᭮";i:230;s:3:"᭯";i:230;s:3:"᭰";i:230;s:3:"᭱";i:230;s:3:"᭲";i:230;s:3:"᭳";i:230;s:3:"᮪";i:9;s:3:"᰷";i:7;s:3:"᷀";i:230;s:3:"᷁";i:230;s:3:"᷂";i:220;s:3:"᷃";i:230;s:3:"᷄";i:230;s:3:"᷅";i:230;s:3:"᷆";i:230;s:3:"᷇";i:230;s:3:"᷈";i:230;s:3:"᷉";i:230;s:3:"᷊";i:220;s:3:"᷋";i:230;s:3:"᷌";i:230;s:3:"᷍";i:234;s:3:"᷎";i:214;s:3:"᷏";i:220;s:3:"᷐";i:202;s:3:"᷑";i:230;s:3:"᷒";i:230;s:3:"ᷓ";i:230;s:3:"ᷔ";i:230;s:3:"ᷕ";i:230;s:3:"ᷖ";i:230;s:3:"ᷗ";i:230;s:3:"ᷘ";i:230;s:3:"ᷙ";i:230;s:3:"ᷚ";i:230;s:3:"ᷛ";i:230;s:3:"ᷜ";i:230;s:3:"ᷝ";i:230;s:3:"ᷞ";i:230;s:3:"ᷟ";i:230;s:3:"ᷠ";i:230;s:3:"ᷡ";i:230;s:3:"ᷢ";i:230;s:3:"ᷣ";i:230;s:3:"ᷤ";i:230;s:3:"ᷥ";i:230;s:3:"ᷦ";i:230;s:3:"᷾";i:230;s:3:"᷿";i:220;s:3:"⃐";i:230;s:3:"⃑";i:230;s:3:"⃒";i:1;s:3:"⃓";i:1;s:3:"⃔";i:230;s:3:"⃕";i:230;s:3:"⃖";i:230;s:3:"⃗";i:230;s:3:"⃘";i:1;s:3:"⃙";i:1;s:3:"⃚";i:1;s:3:"⃛";i:230;s:3:"⃜";i:230;s:3:"⃡";i:230;s:3:"⃥";i:1;s:3:"⃦";i:1;s:3:"⃧";i:230;s:3:"⃨";i:220;s:3:"⃩";i:230;s:3:"⃪";i:1;s:3:"⃫";i:1;s:3:"⃬";i:220;s:3:"⃭";i:220;s:3:"⃮";i:220;s:3:"⃯";i:220;s:3:"⃰";i:230;s:3:"ⷠ";i:230;s:3:"ⷡ";i:230;s:3:"ⷢ";i:230;s:3:"ⷣ";i:230;s:3:"ⷤ";i:230;s:3:"ⷥ";i:230;s:3:"ⷦ";i:230;s:3:"ⷧ";i:230;s:3:"ⷨ";i:230;s:3:"ⷩ";i:230;s:3:"ⷪ";i:230;s:3:"ⷫ";i:230;s:3:"ⷬ";i:230;s:3:"ⷭ";i:230;s:3:"ⷮ";i:230;s:3:"ⷯ";i:230;s:3:"ⷰ";i:230;s:3:"ⷱ";i:230;s:3:"ⷲ";i:230;s:3:"ⷳ";i:230;s:3:"ⷴ";i:230;s:3:"ⷵ";i:230;s:3:"ⷶ";i:230;s:3:"ⷷ";i:230;s:3:"ⷸ";i:230;s:3:"ⷹ";i:230;s:3:"ⷺ";i:230;s:3:"ⷻ";i:230;s:3:"ⷼ";i:230;s:3:"ⷽ";i:230;s:3:"ⷾ";i:230;s:3:"ⷿ";i:230;s:3:"〪";i:218;s:3:"〫";i:228;s:3:"〬";i:232;s:3:"〭";i:222;s:3:"〮";i:224;s:3:"〯";i:224;s:3:"゙";i:8;s:3:"゚";i:8;s:3:"꙯";i:230;s:3:"꙼";i:230;s:3:"꙽";i:230;s:3:"꠆";i:9;s:3:"꣄";i:9;s:3:"꤫";i:220;s:3:"꤬";i:220;s:3:"꤭";i:220;s:3:"꥓";i:9;s:3:"ﬞ";i:26;s:3:"︠";i:230;s:3:"︡";i:230;s:3:"︢";i:230;s:3:"︣";i:230;s:3:"︤";i:230;s:3:"︥";i:230;s:3:"︦";i:230;s:4:"𐇽";i:220;s:4:"𐨍";i:220;s:4:"𐨏";i:230;s:4:"𐨸";i:230;s:4:"𐨹";i:1;s:4:"𐨺";i:220;s:4:"𐨿";i:9;s:4:"𝅥";i:216;s:4:"𝅦";i:216;s:4:"𝅧";i:1;s:4:"𝅨";i:1;s:4:"𝅩";i:1;s:4:"𝅭";i:226;s:4:"𝅮";i:216;s:4:"𝅯";i:216;s:4:"𝅰";i:216;s:4:"𝅱";i:216;s:4:"𝅲";i:216;s:4:"𝅻";i:220;s:4:"𝅼";i:220;s:4:"𝅽";i:220;s:4:"𝅾";i:220;s:4:"𝅿";i:220;s:4:"𝆀";i:220;s:4:"𝆁";i:220;s:4:"𝆂";i:220;s:4:"𝆅";i:230;s:4:"𝆆";i:230;s:4:"𝆇";i:230;s:4:"𝆈";i:230;s:4:"𝆉";i:230;s:4:"𝆊";i:220;s:4:"𝆋";i:220;s:4:"𝆪";i:230;s:4:"𝆫";i:230;s:4:"𝆬";i:230;s:4:"𝆭";i:230;s:4:"𝉂";i:230;s:4:"𝉃";i:230;s:4:"𝉄";i:230;}' );
$utfCanonicalComp = unserialize( 'a:1862:{s:3:"À";s:2:"À";s:3:"Á";s:2:"Á";s:3:"Â";s:2:"Â";s:3:"Ã";s:2:"Ã";s:3:"Ä";s:2:"Ä";s:3:"Å";s:2:"Å";s:3:"Ç";s:2:"Ç";s:3:"È";s:2:"È";s:3:"É";s:2:"É";s:3:"Ê";s:2:"Ê";s:3:"Ë";s:2:"Ë";s:3:"Ì";s:2:"Ì";s:3:"Í";s:2:"Í";s:3:"Î";s:2:"Î";s:3:"Ï";s:2:"Ï";s:3:"Ñ";s:2:"Ñ";s:3:"Ò";s:2:"Ò";s:3:"Ó";s:2:"Ó";s:3:"Ô";s:2:"Ô";s:3:"Õ";s:2:"Õ";s:3:"Ö";s:2:"Ö";s:3:"Ù";s:2:"Ù";s:3:"Ú";s:2:"Ú";s:3:"Û";s:2:"Û";s:3:"Ü";s:2:"Ü";s:3:"Ý";s:2:"Ý";s:3:"à";s:2:"à";s:3:"á";s:2:"á";s:3:"â";s:2:"â";s:3:"ã";s:2:"ã";s:3:"ä";s:2:"ä";s:3:"å";s:2:"å";s:3:"ç";s:2:"ç";s:3:"è";s:2:"è";s:3:"é";s:2:"é";s:3:"ê";s:2:"ê";s:3:"ë";s:2:"ë";s:3:"ì";s:2:"ì";s:3:"í";s:2:"í";s:3:"î";s:2:"î";s:3:"ï";s:2:"ï";s:3:"ñ";s:2:"ñ";s:3:"ò";s:2:"ò";s:3:"ó";s:2:"ó";s:3:"ô";s:2:"ô";s:3:"õ";s:2:"õ";s:3:"ö";s:2:"ö";s:3:"ù";s:2:"ù";s:3:"ú";s:2:"ú";s:3:"û";s:2:"û";s:3:"ü";s:2:"ü";s:3:"ý";s:2:"ý";s:3:"ÿ";s:2:"ÿ";s:3:"Ā";s:2:"Ā";s:3:"ā";s:2:"ā";s:3:"Ă";s:2:"Ă";s:3:"ă";s:2:"ă";s:3:"Ą";s:2:"Ą";s:3:"ą";s:2:"ą";s:3:"Ć";s:2:"Ć";s:3:"ć";s:2:"ć";s:3:"Ĉ";s:2:"Ĉ";s:3:"ĉ";s:2:"ĉ";s:3:"Ċ";s:2:"Ċ";s:3:"ċ";s:2:"ċ";s:3:"Č";s:2:"Č";s:3:"č";s:2:"č";s:3:"Ď";s:2:"Ď";s:3:"ď";s:2:"ď";s:3:"Ē";s:2:"Ē";s:3:"ē";s:2:"ē";s:3:"Ĕ";s:2:"Ĕ";s:3:"ĕ";s:2:"ĕ";s:3:"Ė";s:2:"Ė";s:3:"ė";s:2:"ė";s:3:"Ę";s:2:"Ę";s:3:"ę";s:2:"ę";s:3:"Ě";s:2:"Ě";s:3:"ě";s:2:"ě";s:3:"Ĝ";s:2:"Ĝ";s:3:"ĝ";s:2:"ĝ";s:3:"Ğ";s:2:"Ğ";s:3:"ğ";s:2:"ğ";s:3:"Ġ";s:2:"Ġ";s:3:"ġ";s:2:"ġ";s:3:"Ģ";s:2:"Ģ";s:3:"ģ";s:2:"ģ";s:3:"Ĥ";s:2:"Ĥ";s:3:"ĥ";s:2:"ĥ";s:3:"Ĩ";s:2:"Ĩ";s:3:"ĩ";s:2:"ĩ";s:3:"Ī";s:2:"Ī";s:3:"ī";s:2:"ī";s:3:"Ĭ";s:2:"Ĭ";s:3:"ĭ";s:2:"ĭ";s:3:"Į";s:2:"Į";s:3:"į";s:2:"į";s:3:"İ";s:2:"İ";s:3:"Ĵ";s:2:"Ĵ";s:3:"ĵ";s:2:"ĵ";s:3:"Ķ";s:2:"Ķ";s:3:"ķ";s:2:"ķ";s:3:"Ĺ";s:2:"Ĺ";s:3:"ĺ";s:2:"ĺ";s:3:"Ļ";s:2:"Ļ";s:3:"ļ";s:2:"ļ";s:3:"Ľ";s:2:"Ľ";s:3:"ľ";s:2:"ľ";s:3:"Ń";s:2:"Ń";s:3:"ń";s:2:"ń";s:3:"Ņ";s:2:"Ņ";s:3:"ņ";s:2:"ņ";s:3:"Ň";s:2:"Ň";s:3:"ň";s:2:"ň";s:3:"Ō";s:2:"Ō";s:3:"ō";s:2:"ō";s:3:"Ŏ";s:2:"Ŏ";s:3:"ŏ";s:2:"ŏ";s:3:"Ő";s:2:"Ő";s:3:"ő";s:2:"ő";s:3:"Ŕ";s:2:"Ŕ";s:3:"ŕ";s:2:"ŕ";s:3:"Ŗ";s:2:"Ŗ";s:3:"ŗ";s:2:"ŗ";s:3:"Ř";s:2:"Ř";s:3:"ř";s:2:"ř";s:3:"Ś";s:2:"Ś";s:3:"ś";s:2:"ś";s:3:"Ŝ";s:2:"Ŝ";s:3:"ŝ";s:2:"ŝ";s:3:"Ş";s:2:"Ş";s:3:"ş";s:2:"ş";s:3:"Š";s:2:"Š";s:3:"š";s:2:"š";s:3:"Ţ";s:2:"Ţ";s:3:"ţ";s:2:"ţ";s:3:"Ť";s:2:"Ť";s:3:"ť";s:2:"ť";s:3:"Ũ";s:2:"Ũ";s:3:"ũ";s:2:"ũ";s:3:"Ū";s:2:"Ū";s:3:"ū";s:2:"ū";s:3:"Ŭ";s:2:"Ŭ";s:3:"ŭ";s:2:"ŭ";s:3:"Ů";s:2:"Ů";s:3:"ů";s:2:"ů";s:3:"Ű";s:2:"Ű";s:3:"ű";s:2:"ű";s:3:"Ų";s:2:"Ų";s:3:"ų";s:2:"ų";s:3:"Ŵ";s:2:"Ŵ";s:3:"ŵ";s:2:"ŵ";s:3:"Ŷ";s:2:"Ŷ";s:3:"ŷ";s:2:"ŷ";s:3:"Ÿ";s:2:"Ÿ";s:3:"Ź";s:2:"Ź";s:3:"ź";s:2:"ź";s:3:"Ż";s:2:"Ż";s:3:"ż";s:2:"ż";s:3:"Ž";s:2:"Ž";s:3:"ž";s:2:"ž";s:3:"Ơ";s:2:"Ơ";s:3:"ơ";s:2:"ơ";s:3:"Ư";s:2:"Ư";s:3:"ư";s:2:"ư";s:3:"Ǎ";s:2:"Ǎ";s:3:"ǎ";s:2:"ǎ";s:3:"Ǐ";s:2:"Ǐ";s:3:"ǐ";s:2:"ǐ";s:3:"Ǒ";s:2:"Ǒ";s:3:"ǒ";s:2:"ǒ";s:3:"Ǔ";s:2:"Ǔ";s:3:"ǔ";s:2:"ǔ";s:4:"Ǖ";s:2:"Ǖ";s:4:"ǖ";s:2:"ǖ";s:4:"Ǘ";s:2:"Ǘ";s:4:"ǘ";s:2:"ǘ";s:4:"Ǚ";s:2:"Ǚ";s:4:"ǚ";s:2:"ǚ";s:4:"Ǜ";s:2:"Ǜ";s:4:"ǜ";s:2:"ǜ";s:4:"Ǟ";s:2:"Ǟ";s:4:"ǟ";s:2:"ǟ";s:4:"Ǡ";s:2:"Ǡ";s:4:"ǡ";s:2:"ǡ";s:4:"Ǣ";s:2:"Ǣ";s:4:"ǣ";s:2:"ǣ";s:3:"Ǧ";s:2:"Ǧ";s:3:"ǧ";s:2:"ǧ";s:3:"Ǩ";s:2:"Ǩ";s:3:"ǩ";s:2:"ǩ";s:3:"Ǫ";s:2:"Ǫ";s:3:"ǫ";s:2:"ǫ";s:4:"Ǭ";s:2:"Ǭ";s:4:"ǭ";s:2:"ǭ";s:4:"Ǯ";s:2:"Ǯ";s:4:"ǯ";s:2:"ǯ";s:3:"ǰ";s:2:"ǰ";s:3:"Ǵ";s:2:"Ǵ";s:3:"ǵ";s:2:"ǵ";s:3:"Ǹ";s:2:"Ǹ";s:3:"ǹ";s:2:"ǹ";s:4:"Ǻ";s:2:"Ǻ";s:4:"ǻ";s:2:"ǻ";s:4:"Ǽ";s:2:"Ǽ";s:4:"ǽ";s:2:"ǽ";s:4:"Ǿ";s:2:"Ǿ";s:4:"ǿ";s:2:"ǿ";s:3:"Ȁ";s:2:"Ȁ";s:3:"ȁ";s:2:"ȁ";s:3:"Ȃ";s:2:"Ȃ";s:3:"ȃ";s:2:"ȃ";s:3:"Ȅ";s:2:"Ȅ";s:3:"ȅ";s:2:"ȅ";s:3:"Ȇ";s:2:"Ȇ";s:3:"ȇ";s:2:"ȇ";s:3:"Ȉ";s:2:"Ȉ";s:3:"ȉ";s:2:"ȉ";s:3:"Ȋ";s:2:"Ȋ";s:3:"ȋ";s:2:"ȋ";s:3:"Ȍ";s:2:"Ȍ";s:3:"ȍ";s:2:"ȍ";s:3:"Ȏ";s:2:"Ȏ";s:3:"ȏ";s:2:"ȏ";s:3:"Ȑ";s:2:"Ȑ";s:3:"ȑ";s:2:"ȑ";s:3:"Ȓ";s:2:"Ȓ";s:3:"ȓ";s:2:"ȓ";s:3:"Ȕ";s:2:"Ȕ";s:3:"ȕ";s:2:"ȕ";s:3:"Ȗ";s:2:"Ȗ";s:3:"ȗ";s:2:"ȗ";s:3:"Ș";s:2:"Ș";s:3:"ș";s:2:"ș";s:3:"Ț";s:2:"Ț";s:3:"ț";s:2:"ț";s:3:"Ȟ";s:2:"Ȟ";s:3:"ȟ";s:2:"ȟ";s:3:"Ȧ";s:2:"Ȧ";s:3:"ȧ";s:2:"ȧ";s:3:"Ȩ";s:2:"Ȩ";s:3:"ȩ";s:2:"ȩ";s:4:"Ȫ";s:2:"Ȫ";s:4:"ȫ";s:2:"ȫ";s:4:"Ȭ";s:2:"Ȭ";s:4:"ȭ";s:2:"ȭ";s:3:"Ȯ";s:2:"Ȯ";s:3:"ȯ";s:2:"ȯ";s:4:"Ȱ";s:2:"Ȱ";s:4:"ȱ";s:2:"ȱ";s:3:"Ȳ";s:2:"Ȳ";s:3:"ȳ";s:2:"ȳ";s:2:"̀";s:2:"̀";s:2:"́";s:2:"́";s:2:"̓";s:2:"̓";s:4:"̈́";s:2:"̈́";s:2:"ʹ";s:2:"ʹ";s:1:";";s:2:";";s:4:"΅";s:2:"΅";s:4:"Ά";s:2:"Ά";s:2:"·";s:2:"·";s:4:"Έ";s:2:"Έ";s:4:"Ή";s:2:"Ή";s:4:"Ί";s:2:"Ί";s:4:"Ό";s:2:"Ό";s:4:"Ύ";s:2:"Ύ";s:4:"Ώ";s:2:"Ώ";s:4:"ΐ";s:2:"ΐ";s:4:"Ϊ";s:2:"Ϊ";s:4:"Ϋ";s:2:"Ϋ";s:4:"ά";s:2:"ά";s:4:"έ";s:2:"έ";s:4:"ή";s:2:"ή";s:4:"ί";s:2:"ί";s:4:"ΰ";s:2:"ΰ";s:4:"ϊ";s:2:"ϊ";s:4:"ϋ";s:2:"ϋ";s:4:"ό";s:2:"ό";s:4:"ύ";s:2:"ύ";s:4:"ώ";s:2:"ώ";s:4:"ϓ";s:2:"ϓ";s:4:"ϔ";s:2:"ϔ";s:4:"Ѐ";s:2:"Ѐ";s:4:"Ё";s:2:"Ё";s:4:"Ѓ";s:2:"Ѓ";s:4:"Ї";s:2:"Ї";s:4:"Ќ";s:2:"Ќ";s:4:"Ѝ";s:2:"Ѝ";s:4:"Ў";s:2:"Ў";s:4:"Й";s:2:"Й";s:4:"й";s:2:"й";s:4:"ѐ";s:2:"ѐ";s:4:"ё";s:2:"ё";s:4:"ѓ";s:2:"ѓ";s:4:"ї";s:2:"ї";s:4:"ќ";s:2:"ќ";s:4:"ѝ";s:2:"ѝ";s:4:"ў";s:2:"ў";s:4:"Ѷ";s:2:"Ѷ";s:4:"ѷ";s:2:"ѷ";s:4:"Ӂ";s:2:"Ӂ";s:4:"ӂ";s:2:"ӂ";s:4:"Ӑ";s:2:"Ӑ";s:4:"ӑ";s:2:"ӑ";s:4:"Ӓ";s:2:"Ӓ";s:4:"ӓ";s:2:"ӓ";s:4:"Ӗ";s:2:"Ӗ";s:4:"ӗ";s:2:"ӗ";s:4:"Ӛ";s:2:"Ӛ";s:4:"ӛ";s:2:"ӛ";s:4:"Ӝ";s:2:"Ӝ";s:4:"ӝ";s:2:"ӝ";s:4:"Ӟ";s:2:"Ӟ";s:4:"ӟ";s:2:"ӟ";s:4:"Ӣ";s:2:"Ӣ";s:4:"ӣ";s:2:"ӣ";s:4:"Ӥ";s:2:"Ӥ";s:4:"ӥ";s:2:"ӥ";s:4:"Ӧ";s:2:"Ӧ";s:4:"ӧ";s:2:"ӧ";s:4:"Ӫ";s:2:"Ӫ";s:4:"ӫ";s:2:"ӫ";s:4:"Ӭ";s:2:"Ӭ";s:4:"ӭ";s:2:"ӭ";s:4:"Ӯ";s:2:"Ӯ";s:4:"ӯ";s:2:"ӯ";s:4:"Ӱ";s:2:"Ӱ";s:4:"ӱ";s:2:"ӱ";s:4:"Ӳ";s:2:"Ӳ";s:4:"ӳ";s:2:"ӳ";s:4:"Ӵ";s:2:"Ӵ";s:4:"ӵ";s:2:"ӵ";s:4:"Ӹ";s:2:"Ӹ";s:4:"ӹ";s:2:"ӹ";s:4:"آ";s:2:"آ";s:4:"أ";s:2:"أ";s:4:"ؤ";s:2:"ؤ";s:4:"إ";s:2:"إ";s:4:"ئ";s:2:"ئ";s:4:"ۀ";s:2:"ۀ";s:4:"ۂ";s:2:"ۂ";s:4:"ۓ";s:2:"ۓ";s:6:"ऩ";s:3:"ऩ";s:6:"ऱ";s:3:"ऱ";s:6:"ऴ";s:3:"ऴ";s:6:"ো";s:3:"ো";s:6:"ৌ";s:3:"ৌ";s:6:"ୈ";s:3:"ୈ";s:6:"ୋ";s:3:"ୋ";s:6:"ୌ";s:3:"ୌ";s:6:"ஔ";s:3:"ஔ";s:6:"ொ";s:3:"ொ";s:6:"ோ";s:3:"ோ";s:6:"ௌ";s:3:"ௌ";s:6:"ై";s:3:"ై";s:6:"ೀ";s:3:"ೀ";s:6:"ೇ";s:3:"ೇ";s:6:"ೈ";s:3:"ೈ";s:6:"ೊ";s:3:"ೊ";s:6:"ೋ";s:3:"ೋ";s:6:"ൊ";s:3:"ൊ";s:6:"ോ";s:3:"ോ";s:6:"ൌ";s:3:"ൌ";s:6:"ේ";s:3:"ේ";s:6:"ො";s:3:"ො";s:6:"ෝ";s:3:"ෝ";s:6:"ෞ";s:3:"ෞ";s:6:"ཱི";s:3:"ཱི";s:6:"ཱུ";s:3:"ཱུ";s:6:"ཱྀ";s:3:"ཱྀ";s:6:"ဦ";s:3:"ဦ";s:6:"ᬆ";s:3:"ᬆ";s:6:"ᬈ";s:3:"ᬈ";s:6:"ᬊ";s:3:"ᬊ";s:6:"ᬌ";s:3:"ᬌ";s:6:"ᬎ";s:3:"ᬎ";s:6:"ᬒ";s:3:"ᬒ";s:6:"ᬻ";s:3:"ᬻ";s:6:"ᬽ";s:3:"ᬽ";s:6:"ᭀ";s:3:"ᭀ";s:6:"ᭁ";s:3:"ᭁ";s:6:"ᭃ";s:3:"ᭃ";s:3:"Ḁ";s:3:"Ḁ";s:3:"ḁ";s:3:"ḁ";s:3:"Ḃ";s:3:"Ḃ";s:3:"ḃ";s:3:"ḃ";s:3:"Ḅ";s:3:"Ḅ";s:3:"ḅ";s:3:"ḅ";s:3:"Ḇ";s:3:"Ḇ";s:3:"ḇ";s:3:"ḇ";s:4:"Ḉ";s:3:"Ḉ";s:4:"ḉ";s:3:"ḉ";s:3:"Ḋ";s:3:"Ḋ";s:3:"ḋ";s:3:"ḋ";s:3:"Ḍ";s:3:"Ḍ";s:3:"ḍ";s:3:"ḍ";s:3:"Ḏ";s:3:"Ḏ";s:3:"ḏ";s:3:"ḏ";s:3:"Ḑ";s:3:"Ḑ";s:3:"ḑ";s:3:"ḑ";s:3:"Ḓ";s:3:"Ḓ";s:3:"ḓ";s:3:"ḓ";s:4:"Ḕ";s:3:"Ḕ";s:4:"ḕ";s:3:"ḕ";s:4:"Ḗ";s:3:"Ḗ";s:4:"ḗ";s:3:"ḗ";s:3:"Ḙ";s:3:"Ḙ";s:3:"ḙ";s:3:"ḙ";s:3:"Ḛ";s:3:"Ḛ";s:3:"ḛ";s:3:"ḛ";s:4:"Ḝ";s:3:"Ḝ";s:4:"ḝ";s:3:"ḝ";s:3:"Ḟ";s:3:"Ḟ";s:3:"ḟ";s:3:"ḟ";s:3:"Ḡ";s:3:"Ḡ";s:3:"ḡ";s:3:"ḡ";s:3:"Ḣ";s:3:"Ḣ";s:3:"ḣ";s:3:"ḣ";s:3:"Ḥ";s:3:"Ḥ";s:3:"ḥ";s:3:"ḥ";s:3:"Ḧ";s:3:"Ḧ";s:3:"ḧ";s:3:"ḧ";s:3:"Ḩ";s:3:"Ḩ";s:3:"ḩ";s:3:"ḩ";s:3:"Ḫ";s:3:"Ḫ";s:3:"ḫ";s:3:"ḫ";s:3:"Ḭ";s:3:"Ḭ";s:3:"ḭ";s:3:"ḭ";s:4:"Ḯ";s:3:"Ḯ";s:4:"ḯ";s:3:"ḯ";s:3:"Ḱ";s:3:"Ḱ";s:3:"ḱ";s:3:"ḱ";s:3:"Ḳ";s:3:"Ḳ";s:3:"ḳ";s:3:"ḳ";s:3:"Ḵ";s:3:"Ḵ";s:3:"ḵ";s:3:"ḵ";s:3:"Ḷ";s:3:"Ḷ";s:3:"ḷ";s:3:"ḷ";s:5:"Ḹ";s:3:"Ḹ";s:5:"ḹ";s:3:"ḹ";s:3:"Ḻ";s:3:"Ḻ";s:3:"ḻ";s:3:"ḻ";s:3:"Ḽ";s:3:"Ḽ";s:3:"ḽ";s:3:"ḽ";s:3:"Ḿ";s:3:"Ḿ";s:3:"ḿ";s:3:"ḿ";s:3:"Ṁ";s:3:"Ṁ";s:3:"ṁ";s:3:"ṁ";s:3:"Ṃ";s:3:"Ṃ";s:3:"ṃ";s:3:"ṃ";s:3:"Ṅ";s:3:"Ṅ";s:3:"ṅ";s:3:"ṅ";s:3:"Ṇ";s:3:"Ṇ";s:3:"ṇ";s:3:"ṇ";s:3:"Ṉ";s:3:"Ṉ";s:3:"ṉ";s:3:"ṉ";s:3:"Ṋ";s:3:"Ṋ";s:3:"ṋ";s:3:"ṋ";s:4:"Ṍ";s:3:"Ṍ";s:4:"ṍ";s:3:"ṍ";s:4:"Ṏ";s:3:"Ṏ";s:4:"ṏ";s:3:"ṏ";s:4:"Ṑ";s:3:"Ṑ";s:4:"ṑ";s:3:"ṑ";s:4:"Ṓ";s:3:"Ṓ";s:4:"ṓ";s:3:"ṓ";s:3:"Ṕ";s:3:"Ṕ";s:3:"ṕ";s:3:"ṕ";s:3:"Ṗ";s:3:"Ṗ";s:3:"ṗ";s:3:"ṗ";s:3:"Ṙ";s:3:"Ṙ";s:3:"ṙ";s:3:"ṙ";s:3:"Ṛ";s:3:"Ṛ";s:3:"ṛ";s:3:"ṛ";s:5:"Ṝ";s:3:"Ṝ";s:5:"ṝ";s:3:"ṝ";s:3:"Ṟ";s:3:"Ṟ";s:3:"ṟ";s:3:"ṟ";s:3:"Ṡ";s:3:"Ṡ";s:3:"ṡ";s:3:"ṡ";s:3:"Ṣ";s:3:"Ṣ";s:3:"ṣ";s:3:"ṣ";s:4:"Ṥ";s:3:"Ṥ";s:4:"ṥ";s:3:"ṥ";s:4:"Ṧ";s:3:"Ṧ";s:4:"ṧ";s:3:"ṧ";s:5:"Ṩ";s:3:"Ṩ";s:5:"ṩ";s:3:"ṩ";s:3:"Ṫ";s:3:"Ṫ";s:3:"ṫ";s:3:"ṫ";s:3:"Ṭ";s:3:"Ṭ";s:3:"ṭ";s:3:"ṭ";s:3:"Ṯ";s:3:"Ṯ";s:3:"ṯ";s:3:"ṯ";s:3:"Ṱ";s:3:"Ṱ";s:3:"ṱ";s:3:"ṱ";s:3:"Ṳ";s:3:"Ṳ";s:3:"ṳ";s:3:"ṳ";s:3:"Ṵ";s:3:"Ṵ";s:3:"ṵ";s:3:"ṵ";s:3:"Ṷ";s:3:"Ṷ";s:3:"ṷ";s:3:"ṷ";s:4:"Ṹ";s:3:"Ṹ";s:4:"ṹ";s:3:"ṹ";s:4:"Ṻ";s:3:"Ṻ";s:4:"ṻ";s:3:"ṻ";s:3:"Ṽ";s:3:"Ṽ";s:3:"ṽ";s:3:"ṽ";s:3:"Ṿ";s:3:"Ṿ";s:3:"ṿ";s:3:"ṿ";s:3:"Ẁ";s:3:"Ẁ";s:3:"ẁ";s:3:"ẁ";s:3:"Ẃ";s:3:"Ẃ";s:3:"ẃ";s:3:"ẃ";s:3:"Ẅ";s:3:"Ẅ";s:3:"ẅ";s:3:"ẅ";s:3:"Ẇ";s:3:"Ẇ";s:3:"ẇ";s:3:"ẇ";s:3:"Ẉ";s:3:"Ẉ";s:3:"ẉ";s:3:"ẉ";s:3:"Ẋ";s:3:"Ẋ";s:3:"ẋ";s:3:"ẋ";s:3:"Ẍ";s:3:"Ẍ";s:3:"ẍ";s:3:"ẍ";s:3:"Ẏ";s:3:"Ẏ";s:3:"ẏ";s:3:"ẏ";s:3:"Ẑ";s:3:"Ẑ";s:3:"ẑ";s:3:"ẑ";s:3:"Ẓ";s:3:"Ẓ";s:3:"ẓ";s:3:"ẓ";s:3:"Ẕ";s:3:"Ẕ";s:3:"ẕ";s:3:"ẕ";s:3:"ẖ";s:3:"ẖ";s:3:"ẗ";s:3:"ẗ";s:3:"ẘ";s:3:"ẘ";s:3:"ẙ";s:3:"ẙ";s:4:"ẛ";s:3:"ẛ";s:3:"Ạ";s:3:"Ạ";s:3:"ạ";s:3:"ạ";s:3:"Ả";s:3:"Ả";s:3:"ả";s:3:"ả";s:4:"Ấ";s:3:"Ấ";s:4:"ấ";s:3:"ấ";s:4:"Ầ";s:3:"Ầ";s:4:"ầ";s:3:"ầ";s:4:"Ẩ";s:3:"Ẩ";s:4:"ẩ";s:3:"ẩ";s:4:"Ẫ";s:3:"Ẫ";s:4:"ẫ";s:3:"ẫ";s:5:"Ậ";s:3:"Ậ";s:5:"ậ";s:3:"ậ";s:4:"Ắ";s:3:"Ắ";s:4:"ắ";s:3:"ắ";s:4:"Ằ";s:3:"Ằ";s:4:"ằ";s:3:"ằ";s:4:"Ẳ";s:3:"Ẳ";s:4:"ẳ";s:3:"ẳ";s:4:"Ẵ";s:3:"Ẵ";s:4:"ẵ";s:3:"ẵ";s:5:"Ặ";s:3:"Ặ";s:5:"ặ";s:3:"ặ";s:3:"Ẹ";s:3:"Ẹ";s:3:"ẹ";s:3:"ẹ";s:3:"Ẻ";s:3:"Ẻ";s:3:"ẻ";s:3:"ẻ";s:3:"Ẽ";s:3:"Ẽ";s:3:"ẽ";s:3:"ẽ";s:4:"Ế";s:3:"Ế";s:4:"ế";s:3:"ế";s:4:"Ề";s:3:"Ề";s:4:"ề";s:3:"ề";s:4:"Ể";s:3:"Ể";s:4:"ể";s:3:"ể";s:4:"Ễ";s:3:"Ễ";s:4:"ễ";s:3:"ễ";s:5:"Ệ";s:3:"Ệ";s:5:"ệ";s:3:"ệ";s:3:"Ỉ";s:3:"Ỉ";s:3:"ỉ";s:3:"ỉ";s:3:"Ị";s:3:"Ị";s:3:"ị";s:3:"ị";s:3:"Ọ";s:3:"Ọ";s:3:"ọ";s:3:"ọ";s:3:"Ỏ";s:3:"Ỏ";s:3:"ỏ";s:3:"ỏ";s:4:"Ố";s:3:"Ố";s:4:"ố";s:3:"ố";s:4:"Ồ";s:3:"Ồ";s:4:"ồ";s:3:"ồ";s:4:"Ổ";s:3:"Ổ";s:4:"ổ";s:3:"ổ";s:4:"Ỗ";s:3:"Ỗ";s:4:"ỗ";s:3:"ỗ";s:5:"Ộ";s:3:"Ộ";s:5:"ộ";s:3:"ộ";s:4:"Ớ";s:3:"Ớ";s:4:"ớ";s:3:"ớ";s:4:"Ờ";s:3:"Ờ";s:4:"ờ";s:3:"ờ";s:4:"Ở";s:3:"Ở";s:4:"ở";s:3:"ở";s:4:"Ỡ";s:3:"Ỡ";s:4:"ỡ";s:3:"ỡ";s:4:"Ợ";s:3:"Ợ";s:4:"ợ";s:3:"ợ";s:3:"Ụ";s:3:"Ụ";s:3:"ụ";s:3:"ụ";s:3:"Ủ";s:3:"Ủ";s:3:"ủ";s:3:"ủ";s:4:"Ứ";s:3:"Ứ";s:4:"ứ";s:3:"ứ";s:4:"Ừ";s:3:"Ừ";s:4:"ừ";s:3:"ừ";s:4:"Ử";s:3:"Ử";s:4:"ử";s:3:"ử";s:4:"Ữ";s:3:"Ữ";s:4:"ữ";s:3:"ữ";s:4:"Ự";s:3:"Ự";s:4:"ự";s:3:"ự";s:3:"Ỳ";s:3:"Ỳ";s:3:"ỳ";s:3:"ỳ";s:3:"Ỵ";s:3:"Ỵ";s:3:"ỵ";s:3:"ỵ";s:3:"Ỷ";s:3:"Ỷ";s:3:"ỷ";s:3:"ỷ";s:3:"Ỹ";s:3:"Ỹ";s:3:"ỹ";s:3:"ỹ";s:4:"ἀ";s:3:"ἀ";s:4:"ἁ";s:3:"ἁ";s:5:"ἂ";s:3:"ἂ";s:5:"ἃ";s:3:"ἃ";s:5:"ἄ";s:3:"ἄ";s:5:"ἅ";s:3:"ἅ";s:5:"ἆ";s:3:"ἆ";s:5:"ἇ";s:3:"ἇ";s:4:"Ἀ";s:3:"Ἀ";s:4:"Ἁ";s:3:"Ἁ";s:5:"Ἂ";s:3:"Ἂ";s:5:"Ἃ";s:3:"Ἃ";s:5:"Ἄ";s:3:"Ἄ";s:5:"Ἅ";s:3:"Ἅ";s:5:"Ἆ";s:3:"Ἆ";s:5:"Ἇ";s:3:"Ἇ";s:4:"ἐ";s:3:"ἐ";s:4:"ἑ";s:3:"ἑ";s:5:"ἒ";s:3:"ἒ";s:5:"ἓ";s:3:"ἓ";s:5:"ἔ";s:3:"ἔ";s:5:"ἕ";s:3:"ἕ";s:4:"Ἐ";s:3:"Ἐ";s:4:"Ἑ";s:3:"Ἑ";s:5:"Ἒ";s:3:"Ἒ";s:5:"Ἓ";s:3:"Ἓ";s:5:"Ἔ";s:3:"Ἔ";s:5:"Ἕ";s:3:"Ἕ";s:4:"ἠ";s:3:"ἠ";s:4:"ἡ";s:3:"ἡ";s:5:"ἢ";s:3:"ἢ";s:5:"ἣ";s:3:"ἣ";s:5:"ἤ";s:3:"ἤ";s:5:"ἥ";s:3:"ἥ";s:5:"ἦ";s:3:"ἦ";s:5:"ἧ";s:3:"ἧ";s:4:"Ἠ";s:3:"Ἠ";s:4:"Ἡ";s:3:"Ἡ";s:5:"Ἢ";s:3:"Ἢ";s:5:"Ἣ";s:3:"Ἣ";s:5:"Ἤ";s:3:"Ἤ";s:5:"Ἥ";s:3:"Ἥ";s:5:"Ἦ";s:3:"Ἦ";s:5:"Ἧ";s:3:"Ἧ";s:4:"ἰ";s:3:"ἰ";s:4:"ἱ";s:3:"ἱ";s:5:"ἲ";s:3:"ἲ";s:5:"ἳ";s:3:"ἳ";s:5:"ἴ";s:3:"ἴ";s:5:"ἵ";s:3:"ἵ";s:5:"ἶ";s:3:"ἶ";s:5:"ἷ";s:3:"ἷ";s:4:"Ἰ";s:3:"Ἰ";s:4:"Ἱ";s:3:"Ἱ";s:5:"Ἲ";s:3:"Ἲ";s:5:"Ἳ";s:3:"Ἳ";s:5:"Ἴ";s:3:"Ἴ";s:5:"Ἵ";s:3:"Ἵ";s:5:"Ἶ";s:3:"Ἶ";s:5:"Ἷ";s:3:"Ἷ";s:4:"ὀ";s:3:"ὀ";s:4:"ὁ";s:3:"ὁ";s:5:"ὂ";s:3:"ὂ";s:5:"ὃ";s:3:"ὃ";s:5:"ὄ";s:3:"ὄ";s:5:"ὅ";s:3:"ὅ";s:4:"Ὀ";s:3:"Ὀ";s:4:"Ὁ";s:3:"Ὁ";s:5:"Ὂ";s:3:"Ὂ";s:5:"Ὃ";s:3:"Ὃ";s:5:"Ὄ";s:3:"Ὄ";s:5:"Ὅ";s:3:"Ὅ";s:4:"ὐ";s:3:"ὐ";s:4:"ὑ";s:3:"ὑ";s:5:"ὒ";s:3:"ὒ";s:5:"ὓ";s:3:"ὓ";s:5:"ὔ";s:3:"ὔ";s:5:"ὕ";s:3:"ὕ";s:5:"ὖ";s:3:"ὖ";s:5:"ὗ";s:3:"ὗ";s:4:"Ὑ";s:3:"Ὑ";s:5:"Ὓ";s:3:"Ὓ";s:5:"Ὕ";s:3:"Ὕ";s:5:"Ὗ";s:3:"Ὗ";s:4:"ὠ";s:3:"ὠ";s:4:"ὡ";s:3:"ὡ";s:5:"ὢ";s:3:"ὢ";s:5:"ὣ";s:3:"ὣ";s:5:"ὤ";s:3:"ὤ";s:5:"ὥ";s:3:"ὥ";s:5:"ὦ";s:3:"ὦ";s:5:"ὧ";s:3:"ὧ";s:4:"Ὠ";s:3:"Ὠ";s:4:"Ὡ";s:3:"Ὡ";s:5:"Ὢ";s:3:"Ὢ";s:5:"Ὣ";s:3:"Ὣ";s:5:"Ὤ";s:3:"Ὤ";s:5:"Ὥ";s:3:"Ὥ";s:5:"Ὦ";s:3:"Ὦ";s:5:"Ὧ";s:3:"Ὧ";s:4:"ὰ";s:3:"ὰ";s:2:"ά";s:3:"ά";s:4:"ὲ";s:3:"ὲ";s:2:"έ";s:3:"έ";s:4:"ὴ";s:3:"ὴ";s:2:"ή";s:3:"ή";s:4:"ὶ";s:3:"ὶ";s:2:"ί";s:3:"ί";s:4:"ὸ";s:3:"ὸ";s:2:"ό";s:3:"ό";s:4:"ὺ";s:3:"ὺ";s:2:"ύ";s:3:"ύ";s:4:"ὼ";s:3:"ὼ";s:2:"ώ";s:3:"ώ";s:5:"ᾀ";s:3:"ᾀ";s:5:"ᾁ";s:3:"ᾁ";s:5:"ᾂ";s:3:"ᾂ";s:5:"ᾃ";s:3:"ᾃ";s:5:"ᾄ";s:3:"ᾄ";s:5:"ᾅ";s:3:"ᾅ";s:5:"ᾆ";s:3:"ᾆ";s:5:"ᾇ";s:3:"ᾇ";s:5:"ᾈ";s:3:"ᾈ";s:5:"ᾉ";s:3:"ᾉ";s:5:"ᾊ";s:3:"ᾊ";s:5:"ᾋ";s:3:"ᾋ";s:5:"ᾌ";s:3:"ᾌ";s:5:"ᾍ";s:3:"ᾍ";s:5:"ᾎ";s:3:"ᾎ";s:5:"ᾏ";s:3:"ᾏ";s:5:"ᾐ";s:3:"ᾐ";s:5:"ᾑ";s:3:"ᾑ";s:5:"ᾒ";s:3:"ᾒ";s:5:"ᾓ";s:3:"ᾓ";s:5:"ᾔ";s:3:"ᾔ";s:5:"ᾕ";s:3:"ᾕ";s:5:"ᾖ";s:3:"ᾖ";s:5:"ᾗ";s:3:"ᾗ";s:5:"ᾘ";s:3:"ᾘ";s:5:"ᾙ";s:3:"ᾙ";s:5:"ᾚ";s:3:"ᾚ";s:5:"ᾛ";s:3:"ᾛ";s:5:"ᾜ";s:3:"ᾜ";s:5:"ᾝ";s:3:"ᾝ";s:5:"ᾞ";s:3:"ᾞ";s:5:"ᾟ";s:3:"ᾟ";s:5:"ᾠ";s:3:"ᾠ";s:5:"ᾡ";s:3:"ᾡ";s:5:"ᾢ";s:3:"ᾢ";s:5:"ᾣ";s:3:"ᾣ";s:5:"ᾤ";s:3:"ᾤ";s:5:"ᾥ";s:3:"ᾥ";s:5:"ᾦ";s:3:"ᾦ";s:5:"ᾧ";s:3:"ᾧ";s:5:"ᾨ";s:3:"ᾨ";s:5:"ᾩ";s:3:"ᾩ";s:5:"ᾪ";s:3:"ᾪ";s:5:"ᾫ";s:3:"ᾫ";s:5:"ᾬ";s:3:"ᾬ";s:5:"ᾭ";s:3:"ᾭ";s:5:"ᾮ";s:3:"ᾮ";s:5:"ᾯ";s:3:"ᾯ";s:4:"ᾰ";s:3:"ᾰ";s:4:"ᾱ";s:3:"ᾱ";s:5:"ᾲ";s:3:"ᾲ";s:4:"ᾳ";s:3:"ᾳ";s:4:"ᾴ";s:3:"ᾴ";s:4:"ᾶ";s:3:"ᾶ";s:5:"ᾷ";s:3:"ᾷ";s:4:"Ᾰ";s:3:"Ᾰ";s:4:"Ᾱ";s:3:"Ᾱ";s:4:"Ὰ";s:3:"Ὰ";s:2:"Ά";s:3:"Ά";s:4:"ᾼ";s:3:"ᾼ";s:2:"ι";s:3:"ι";s:4:"῁";s:3:"῁";s:5:"ῂ";s:3:"ῂ";s:4:"ῃ";s:3:"ῃ";s:4:"ῄ";s:3:"ῄ";s:4:"ῆ";s:3:"ῆ";s:5:"ῇ";s:3:"ῇ";s:4:"Ὲ";s:3:"Ὲ";s:2:"Έ";s:3:"Έ";s:4:"Ὴ";s:3:"Ὴ";s:2:"Ή";s:3:"Ή";s:4:"ῌ";s:3:"ῌ";s:5:"῍";s:3:"῍";s:5:"῎";s:3:"῎";s:5:"῏";s:3:"῏";s:4:"ῐ";s:3:"ῐ";s:4:"ῑ";s:3:"ῑ";s:4:"ῒ";s:3:"ῒ";s:2:"ΐ";s:3:"ΐ";s:4:"ῖ";s:3:"ῖ";s:4:"ῗ";s:3:"ῗ";s:4:"Ῐ";s:3:"Ῐ";s:4:"Ῑ";s:3:"Ῑ";s:4:"Ὶ";s:3:"Ὶ";s:2:"Ί";s:3:"Ί";s:5:"῝";s:3:"῝";s:5:"῞";s:3:"῞";s:5:"῟";s:3:"῟";s:4:"ῠ";s:3:"ῠ";s:4:"ῡ";s:3:"ῡ";s:4:"ῢ";s:3:"ῢ";s:2:"ΰ";s:3:"ΰ";s:4:"ῤ";s:3:"ῤ";s:4:"ῥ";s:3:"ῥ";s:4:"ῦ";s:3:"ῦ";s:4:"ῧ";s:3:"ῧ";s:4:"Ῠ";s:3:"Ῠ";s:4:"Ῡ";s:3:"Ῡ";s:4:"Ὺ";s:3:"Ὺ";s:2:"Ύ";s:3:"Ύ";s:4:"Ῥ";s:3:"Ῥ";s:4:"῭";s:3:"῭";s:2:"΅";s:3:"΅";s:1:"`";s:3:"`";s:5:"ῲ";s:3:"ῲ";s:4:"ῳ";s:3:"ῳ";s:4:"ῴ";s:3:"ῴ";s:4:"ῶ";s:3:"ῶ";s:5:"ῷ";s:3:"ῷ";s:4:"Ὸ";s:3:"Ὸ";s:2:"Ό";s:3:"Ό";s:4:"Ὼ";s:3:"Ὼ";s:2:"Ώ";s:3:"Ώ";s:4:"ῼ";s:3:"ῼ";s:2:"´";s:3:"´";s:3:" ";s:3:" ";s:3:" ";s:3:" ";s:2:"Ω";s:3:"Ω";s:1:"K";s:3:"K";s:2:"Å";s:3:"Å";s:5:"↚";s:3:"↚";s:5:"↛";s:3:"↛";s:5:"↮";s:3:"↮";s:5:"⇍";s:3:"⇍";s:5:"⇎";s:3:"⇎";s:5:"⇏";s:3:"⇏";s:5:"∄";s:3:"∄";s:5:"∉";s:3:"∉";s:5:"∌";s:3:"∌";s:5:"∤";s:3:"∤";s:5:"∦";s:3:"∦";s:5:"≁";s:3:"≁";s:5:"≄";s:3:"≄";s:5:"≇";s:3:"≇";s:5:"≉";s:3:"≉";s:3:"≠";s:3:"≠";s:5:"≢";s:3:"≢";s:5:"≭";s:3:"≭";s:3:"≮";s:3:"≮";s:3:"≯";s:3:"≯";s:5:"≰";s:3:"≰";s:5:"≱";s:3:"≱";s:5:"≴";s:3:"≴";s:5:"≵";s:3:"≵";s:5:"≸";s:3:"≸";s:5:"≹";s:3:"≹";s:5:"⊀";s:3:"⊀";s:5:"⊁";s:3:"⊁";s:5:"⊄";s:3:"⊄";s:5:"⊅";s:3:"⊅";s:5:"⊈";s:3:"⊈";s:5:"⊉";s:3:"⊉";s:5:"⊬";s:3:"⊬";s:5:"⊭";s:3:"⊭";s:5:"⊮";s:3:"⊮";s:5:"⊯";s:3:"⊯";s:5:"⋠";s:3:"⋠";s:5:"⋡";s:3:"⋡";s:5:"⋢";s:3:"⋢";s:5:"⋣";s:3:"⋣";s:5:"⋪";s:3:"⋪";s:5:"⋫";s:3:"⋫";s:5:"⋬";s:3:"⋬";s:5:"⋭";s:3:"⋭";s:3:"〈";s:3:"〈";s:3:"〉";s:3:"〉";s:6:"が";s:3:"が";s:6:"ぎ";s:3:"ぎ";s:6:"ぐ";s:3:"ぐ";s:6:"げ";s:3:"げ";s:6:"ご";s:3:"ご";s:6:"ざ";s:3:"ざ";s:6:"じ";s:3:"じ";s:6:"ず";s:3:"ず";s:6:"ぜ";s:3:"ぜ";s:6:"ぞ";s:3:"ぞ";s:6:"だ";s:3:"だ";s:6:"ぢ";s:3:"ぢ";s:6:"づ";s:3:"づ";s:6:"で";s:3:"で";s:6:"ど";s:3:"ど";s:6:"ば";s:3:"ば";s:6:"ぱ";s:3:"ぱ";s:6:"び";s:3:"び";s:6:"ぴ";s:3:"ぴ";s:6:"ぶ";s:3:"ぶ";s:6:"ぷ";s:3:"ぷ";s:6:"べ";s:3:"べ";s:6:"ぺ";s:3:"ぺ";s:6:"ぼ";s:3:"ぼ";s:6:"ぽ";s:3:"ぽ";s:6:"ゔ";s:3:"ゔ";s:6:"ゞ";s:3:"ゞ";s:6:"ガ";s:3:"ガ";s:6:"ギ";s:3:"ギ";s:6:"グ";s:3:"グ";s:6:"ゲ";s:3:"ゲ";s:6:"ゴ";s:3:"ゴ";s:6:"ザ";s:3:"ザ";s:6:"ジ";s:3:"ジ";s:6:"ズ";s:3:"ズ";s:6:"ゼ";s:3:"ゼ";s:6:"ゾ";s:3:"ゾ";s:6:"ダ";s:3:"ダ";s:6:"ヂ";s:3:"ヂ";s:6:"ヅ";s:3:"ヅ";s:6:"デ";s:3:"デ";s:6:"ド";s:3:"ド";s:6:"バ";s:3:"バ";s:6:"パ";s:3:"パ";s:6:"ビ";s:3:"ビ";s:6:"ピ";s:3:"ピ";s:6:"ブ";s:3:"ブ";s:6:"プ";s:3:"プ";s:6:"ベ";s:3:"ベ";s:6:"ペ";s:3:"ペ";s:6:"ボ";s:3:"ボ";s:6:"ポ";s:3:"ポ";s:6:"ヴ";s:3:"ヴ";s:6:"ヷ";s:3:"ヷ";s:6:"ヸ";s:3:"ヸ";s:6:"ヹ";s:3:"ヹ";s:6:"ヺ";s:3:"ヺ";s:6:"ヾ";s:3:"ヾ";s:3:"豈";s:3:"豈";s:3:"更";s:3:"更";s:3:"車";s:3:"車";s:3:"賈";s:3:"賈";s:3:"滑";s:3:"滑";s:3:"串";s:3:"串";s:3:"句";s:3:"句";s:3:"龜";s:3:"龜";s:3:"契";s:3:"契";s:3:"金";s:3:"金";s:3:"喇";s:3:"喇";s:3:"奈";s:3:"奈";s:3:"懶";s:4:"懶";s:3:"癩";s:3:"癩";s:3:"羅";s:3:"羅";s:3:"蘿";s:3:"蘿";s:3:"螺";s:3:"螺";s:3:"裸";s:3:"裸";s:3:"邏";s:3:"邏";s:3:"樂";s:3:"樂";s:3:"洛";s:3:"洛";s:3:"烙";s:3:"烙";s:3:"珞";s:3:"珞";s:3:"落";s:3:"落";s:3:"酪";s:3:"酪";s:3:"駱";s:3:"駱";s:3:"亂";s:3:"亂";s:3:"卵";s:3:"卵";s:3:"欄";s:3:"欄";s:3:"爛";s:3:"爛";s:3:"蘭";s:3:"蘭";s:3:"鸞";s:3:"鸞";s:3:"嵐";s:3:"嵐";s:3:"濫";s:3:"濫";s:3:"藍";s:3:"藍";s:3:"襤";s:3:"襤";s:3:"拉";s:3:"拉";s:3:"臘";s:3:"臘";s:3:"蠟";s:3:"蠟";s:3:"廊";s:4:"廊";s:3:"朗";s:4:"朗";s:3:"浪";s:3:"浪";s:3:"狼";s:3:"狼";s:3:"郎";s:3:"郎";s:3:"來";s:3:"來";s:3:"冷";s:3:"冷";s:3:"勞";s:3:"勞";s:3:"擄";s:3:"擄";s:3:"櫓";s:3:"櫓";s:3:"爐";s:3:"爐";s:3:"盧";s:3:"盧";s:3:"老";s:3:"老";s:3:"蘆";s:3:"蘆";s:3:"虜";s:4:"虜";s:3:"路";s:3:"路";s:3:"露";s:3:"露";s:3:"魯";s:3:"魯";s:3:"鷺";s:3:"鷺";s:3:"碌";s:4:"碌";s:3:"祿";s:3:"祿";s:3:"綠";s:3:"綠";s:3:"菉";s:3:"菉";s:3:"錄";s:3:"錄";s:3:"鹿";s:3:"鹿";s:3:"論";s:3:"論";s:3:"壟";s:3:"壟";s:3:"弄";s:3:"弄";s:3:"籠";s:3:"籠";s:3:"聾";s:3:"聾";s:3:"牢";s:3:"牢";s:3:"磊";s:3:"磊";s:3:"賂";s:3:"賂";s:3:"雷";s:3:"雷";s:3:"壘";s:3:"壘";s:3:"屢";s:3:"屢";s:3:"樓";s:3:"樓";s:3:"淚";s:3:"淚";s:3:"漏";s:3:"漏";s:3:"累";s:3:"累";s:3:"縷";s:3:"縷";s:3:"陋";s:3:"陋";s:3:"勒";s:3:"勒";s:3:"肋";s:3:"肋";s:3:"凜";s:3:"凜";s:3:"凌";s:3:"凌";s:3:"稜";s:3:"稜";s:3:"綾";s:3:"綾";s:3:"菱";s:3:"菱";s:3:"陵";s:3:"陵";s:3:"讀";s:3:"讀";s:3:"拏";s:3:"拏";s:3:"諾";s:3:"諾";s:3:"丹";s:3:"丹";s:3:"寧";s:4:"寧";s:3:"怒";s:3:"怒";s:3:"率";s:3:"率";s:3:"異";s:4:"異";s:3:"北";s:4:"北";s:3:"磻";s:3:"磻";s:3:"便";s:3:"便";s:3:"復";s:3:"復";s:3:"不";s:3:"不";s:3:"泌";s:3:"泌";s:3:"數";s:3:"數";s:3:"索";s:3:"索";s:3:"參";s:3:"參";s:3:"塞";s:3:"塞";s:3:"省";s:3:"省";s:3:"葉";s:3:"葉";s:3:"說";s:3:"說";s:3:"殺";s:4:"殺";s:3:"辰";s:3:"辰";s:3:"沈";s:3:"沈";s:3:"拾";s:3:"拾";s:3:"若";s:4:"若";s:3:"掠";s:3:"掠";s:3:"略";s:3:"略";s:3:"亮";s:3:"亮";s:3:"兩";s:3:"兩";s:3:"凉";s:3:"凉";s:3:"梁";s:3:"梁";s:3:"糧";s:3:"糧";s:3:"良";s:3:"良";s:3:"諒";s:3:"諒";s:3:"量";s:3:"量";s:3:"勵";s:3:"勵";s:3:"呂";s:3:"呂";s:3:"女";s:3:"女";s:3:"廬";s:3:"廬";s:3:"旅";s:3:"旅";s:3:"濾";s:3:"濾";s:3:"礪";s:3:"礪";s:3:"閭";s:3:"閭";s:3:"驪";s:3:"驪";s:3:"麗";s:3:"麗";s:3:"黎";s:3:"黎";s:3:"力";s:3:"力";s:3:"曆";s:3:"曆";s:3:"歷";s:3:"歷";s:3:"轢";s:3:"轢";s:3:"年";s:3:"年";s:3:"憐";s:3:"憐";s:3:"戀";s:3:"戀";s:3:"撚";s:3:"撚";s:3:"漣";s:3:"漣";s:3:"煉";s:3:"煉";s:3:"璉";s:3:"璉";s:3:"秊";s:3:"秊";s:3:"練";s:3:"練";s:3:"聯";s:3:"聯";s:3:"輦";s:3:"輦";s:3:"蓮";s:3:"蓮";s:3:"連";s:3:"連";s:3:"鍊";s:3:"鍊";s:3:"列";s:3:"列";s:3:"劣";s:3:"劣";s:3:"咽";s:3:"咽";s:3:"烈";s:3:"烈";s:3:"裂";s:3:"裂";s:3:"廉";s:3:"廉";s:3:"念";s:3:"念";s:3:"捻";s:3:"捻";s:3:"殮";s:3:"殮";s:3:"簾";s:3:"簾";s:3:"獵";s:3:"獵";s:3:"令";s:3:"令";s:3:"囹";s:3:"囹";s:3:"嶺";s:3:"嶺";s:3:"怜";s:3:"怜";s:3:"玲";s:3:"玲";s:3:"瑩";s:3:"瑩";s:3:"羚";s:3:"羚";s:3:"聆";s:3:"聆";s:3:"鈴";s:3:"鈴";s:3:"零";s:3:"零";s:3:"靈";s:3:"靈";s:3:"領";s:3:"領";s:3:"例";s:3:"例";s:3:"禮";s:3:"禮";s:3:"醴";s:3:"醴";s:3:"隸";s:3:"隸";s:3:"惡";s:3:"惡";s:3:"了";s:3:"了";s:3:"僚";s:3:"僚";s:3:"寮";s:3:"寮";s:3:"尿";s:3:"尿";s:3:"料";s:3:"料";s:3:"燎";s:3:"燎";s:3:"療";s:3:"療";s:3:"蓼";s:3:"蓼";s:3:"遼";s:3:"遼";s:3:"龍";s:3:"龍";s:3:"暈";s:3:"暈";s:3:"阮";s:3:"阮";s:3:"劉";s:3:"劉";s:3:"杻";s:3:"杻";s:3:"柳";s:3:"柳";s:3:"流";s:4:"流";s:3:"溜";s:3:"溜";s:3:"琉";s:3:"琉";s:3:"留";s:3:"留";s:3:"硫";s:3:"硫";s:3:"紐";s:3:"紐";s:3:"類";s:3:"類";s:3:"六";s:3:"六";s:3:"戮";s:3:"戮";s:3:"陸";s:3:"陸";s:3:"倫";s:3:"倫";s:3:"崙";s:3:"崙";s:3:"淪";s:3:"淪";s:3:"輪";s:3:"輪";s:3:"律";s:3:"律";s:3:"慄";s:3:"慄";s:3:"栗";s:3:"栗";s:3:"隆";s:3:"隆";s:3:"利";s:3:"利";s:3:"吏";s:3:"吏";s:3:"履";s:3:"履";s:3:"易";s:3:"易";s:3:"李";s:3:"李";s:3:"梨";s:3:"梨";s:3:"泥";s:3:"泥";s:3:"理";s:3:"理";s:3:"痢";s:3:"痢";s:3:"罹";s:3:"罹";s:3:"裏";s:3:"裏";s:3:"裡";s:3:"裡";s:3:"里";s:3:"里";s:3:"離";s:3:"離";s:3:"匿";s:3:"匿";s:3:"溺";s:3:"溺";s:3:"吝";s:3:"吝";s:3:"燐";s:3:"燐";s:3:"璘";s:3:"璘";s:3:"藺";s:3:"藺";s:3:"隣";s:3:"隣";s:3:"鱗";s:3:"鱗";s:3:"麟";s:3:"麟";s:3:"林";s:3:"林";s:3:"淋";s:3:"淋";s:3:"臨";s:3:"臨";s:3:"立";s:3:"立";s:3:"笠";s:3:"笠";s:3:"粒";s:3:"粒";s:3:"狀";s:3:"狀";s:3:"炙";s:3:"炙";s:3:"識";s:3:"識";s:3:"什";s:3:"什";s:3:"茶";s:3:"茶";s:3:"刺";s:3:"刺";s:3:"切";s:4:"切";s:3:"度";s:3:"度";s:3:"拓";s:3:"拓";s:3:"糖";s:3:"糖";s:3:"宅";s:3:"宅";s:3:"洞";s:3:"洞";s:3:"暴";s:3:"暴";s:3:"輻";s:3:"輻";s:3:"行";s:3:"行";s:3:"降";s:3:"降";s:3:"見";s:3:"見";s:3:"廓";s:3:"廓";s:3:"兀";s:3:"兀";s:3:"嗀";s:3:"嗀";s:3:"塚";s:3:"塚";s:3:"晴";s:3:"晴";s:3:"凞";s:3:"凞";s:3:"猪";s:3:"猪";s:3:"益";s:3:"益";s:3:"礼";s:3:"礼";s:3:"神";s:3:"神";s:3:"祥";s:3:"祥";s:3:"福";s:4:"福";s:3:"靖";s:3:"靖";s:3:"精";s:3:"精";s:3:"羽";s:3:"羽";s:3:"蘒";s:3:"蘒";s:3:"諸";s:3:"諸";s:3:"逸";s:3:"逸";s:3:"都";s:3:"都";s:3:"飯";s:3:"飯";s:3:"飼";s:3:"飼";s:3:"館";s:3:"館";s:3:"鶴";s:3:"鶴";s:3:"侮";s:4:"侮";s:3:"僧";s:4:"僧";s:3:"免";s:4:"免";s:3:"勉";s:4:"勉";s:3:"勤";s:4:"勤";s:3:"卑";s:4:"卑";s:3:"喝";s:3:"喝";s:3:"嘆";s:4:"嘆";s:3:"器";s:3:"器";s:3:"塀";s:3:"塀";s:3:"墨";s:3:"墨";s:3:"層";s:3:"層";s:3:"屮";s:4:"屮";s:3:"悔";s:4:"悔";s:3:"慨";s:3:"慨";s:3:"憎";s:4:"憎";s:3:"懲";s:4:"懲";s:3:"敏";s:4:"敏";s:3:"既";s:3:"既";s:3:"暑";s:4:"暑";s:3:"梅";s:4:"梅";s:3:"海";s:4:"海";s:3:"渚";s:3:"渚";s:3:"漢";s:3:"漢";s:3:"煮";s:3:"煮";s:3:"爫";s:3:"爫";s:3:"琢";s:3:"琢";s:3:"碑";s:3:"碑";s:3:"社";s:3:"社";s:3:"祉";s:3:"祉";s:3:"祈";s:3:"祈";s:3:"祐";s:3:"祐";s:3:"祖";s:4:"祖";s:3:"祝";s:3:"祝";s:3:"禍";s:3:"禍";s:3:"禎";s:3:"禎";s:3:"穀";s:4:"穀";s:3:"突";s:3:"突";s:3:"節";s:3:"節";s:3:"縉";s:3:"縉";s:3:"繁";s:3:"繁";s:3:"署";s:3:"署";s:3:"者";s:4:"者";s:3:"臭";s:3:"臭";s:3:"艹";s:3:"艹";s:3:"著";s:4:"著";s:3:"褐";s:3:"褐";s:3:"視";s:3:"視";s:3:"謁";s:3:"謁";s:3:"謹";s:3:"謹";s:3:"賓";s:3:"賓";s:3:"贈";s:3:"贈";s:3:"辶";s:3:"辶";s:3:"難";s:3:"難";s:3:"響";s:3:"響";s:3:"頻";s:3:"頻";s:3:"並";s:3:"並";s:3:"况";s:4:"况";s:3:"全";s:3:"全";s:3:"侀";s:3:"侀";s:3:"充";s:3:"充";s:3:"冀";s:3:"冀";s:3:"勇";s:4:"勇";s:3:"勺";s:4:"勺";s:3:"啕";s:3:"啕";s:3:"喙";s:4:"喙";s:3:"嗢";s:3:"嗢";s:3:"墳";s:3:"墳";s:3:"奄";s:3:"奄";s:3:"奔";s:3:"奔";s:3:"婢";s:3:"婢";s:3:"嬨";s:3:"嬨";s:3:"廒";s:3:"廒";s:3:"廙";s:3:"廙";s:3:"彩";s:3:"彩";s:3:"徭";s:3:"徭";s:3:"惘";s:3:"惘";s:3:"慎";s:4:"慎";s:3:"愈";s:3:"愈";s:3:"慠";s:3:"慠";s:3:"戴";s:3:"戴";s:3:"揄";s:3:"揄";s:3:"搜";s:3:"搜";s:3:"摒";s:3:"摒";s:3:"敖";s:3:"敖";s:3:"望";s:4:"望";s:3:"杖";s:3:"杖";s:3:"歹";s:3:"歹";s:3:"滛";s:3:"滛";s:3:"滋";s:4:"滋";s:3:"瀞";s:4:"瀞";s:3:"瞧";s:3:"瞧";s:3:"爵";s:4:"爵";s:3:"犯";s:3:"犯";s:3:"瑱";s:4:"瑱";s:3:"甆";s:3:"甆";s:3:"画";s:3:"画";s:3:"瘝";s:3:"瘝";s:3:"瘟";s:3:"瘟";s:3:"盛";s:3:"盛";s:3:"直";s:4:"直";s:3:"睊";s:4:"睊";s:3:"着";s:3:"着";s:3:"磌";s:4:"磌";s:3:"窱";s:3:"窱";s:3:"类";s:3:"类";s:3:"絛";s:3:"絛";s:3:"缾";s:3:"缾";s:3:"荒";s:3:"荒";s:3:"華";s:3:"華";s:3:"蝹";s:4:"蝹";s:3:"襁";s:3:"襁";s:3:"覆";s:3:"覆";s:3:"調";s:3:"調";s:3:"請";s:3:"請";s:3:"諭";s:4:"諭";s:3:"變";s:4:"變";s:3:"輸";s:4:"輸";s:3:"遲";s:3:"遲";s:3:"醙";s:3:"醙";s:3:"鉶";s:3:"鉶";s:3:"陼";s:3:"陼";s:3:"韛";s:3:"韛";s:3:"頋";s:4:"頋";s:3:"鬒";s:4:"鬒";s:4:"𢡊";s:3:"𢡊";s:4:"𢡄";s:3:"𢡄";s:4:"𣏕";s:3:"𣏕";s:3:"㮝";s:4:"㮝";s:3:"䀘";s:3:"䀘";s:3:"䀹";s:4:"䀹";s:4:"𥉉";s:3:"𥉉";s:4:"𥳐";s:3:"𥳐";s:4:"𧻓";s:3:"𧻓";s:3:"齃";s:3:"齃";s:3:"龎";s:3:"龎";s:3:"丽";s:4:"丽";s:3:"丸";s:4:"丸";s:3:"乁";s:4:"乁";s:4:"𠄢";s:4:"𠄢";s:3:"你";s:4:"你";s:3:"侻";s:4:"侻";s:3:"倂";s:4:"倂";s:3:"偺";s:4:"偺";s:3:"備";s:4:"備";s:3:"像";s:4:"像";s:3:"㒞";s:4:"㒞";s:4:"𠘺";s:4:"𠘺";s:3:"兔";s:4:"兔";s:3:"兤";s:4:"兤";s:3:"具";s:4:"具";s:4:"𠔜";s:4:"𠔜";s:3:"㒹";s:4:"㒹";s:3:"內";s:4:"內";s:3:"再";s:4:"再";s:4:"𠕋";s:4:"𠕋";s:3:"冗";s:4:"冗";s:3:"冤";s:4:"冤";s:3:"仌";s:4:"仌";s:3:"冬";s:4:"冬";s:4:"𩇟";s:4:"𩇟";s:3:"凵";s:4:"凵";s:3:"刃";s:4:"刃";s:3:"㓟";s:4:"㓟";s:3:"刻";s:4:"刻";s:3:"剆";s:4:"剆";s:3:"割";s:4:"割";s:3:"剷";s:4:"剷";s:3:"㔕";s:4:"㔕";s:3:"包";s:4:"包";s:3:"匆";s:4:"匆";s:3:"卉";s:4:"卉";s:3:"博";s:4:"博";s:3:"即";s:4:"即";s:3:"卽";s:4:"卽";s:3:"卿";s:4:"卿";s:4:"𠨬";s:4:"𠨬";s:3:"灰";s:4:"灰";s:3:"及";s:4:"及";s:3:"叟";s:4:"叟";s:4:"𠭣";s:4:"𠭣";s:3:"叫";s:4:"叫";s:3:"叱";s:4:"叱";s:3:"吆";s:4:"吆";s:3:"咞";s:4:"咞";s:3:"吸";s:4:"吸";s:3:"呈";s:4:"呈";s:3:"周";s:4:"周";s:3:"咢";s:4:"咢";s:3:"哶";s:4:"哶";s:3:"唐";s:4:"唐";s:3:"啓";s:4:"啓";s:3:"啣";s:4:"啣";s:3:"善";s:4:"善";s:3:"喫";s:4:"喫";s:3:"喳";s:4:"喳";s:3:"嗂";s:4:"嗂";s:3:"圖";s:4:"圖";s:3:"圗";s:4:"圗";s:3:"噑";s:4:"噑";s:3:"噴";s:4:"噴";s:3:"壮";s:4:"壮";s:3:"城";s:4:"城";s:3:"埴";s:4:"埴";s:3:"堍";s:4:"堍";s:3:"型";s:4:"型";s:3:"堲";s:4:"堲";s:3:"報";s:4:"報";s:3:"墬";s:4:"墬";s:4:"𡓤";s:4:"𡓤";s:3:"売";s:4:"売";s:3:"壷";s:4:"壷";s:3:"夆";s:4:"夆";s:3:"多";s:4:"多";s:3:"夢";s:4:"夢";s:3:"奢";s:4:"奢";s:4:"𡚨";s:4:"𡚨";s:4:"𡛪";s:4:"𡛪";s:3:"姬";s:4:"姬";s:3:"娛";s:4:"娛";s:3:"娧";s:4:"娧";s:3:"姘";s:4:"姘";s:3:"婦";s:4:"婦";s:3:"㛮";s:4:"㛮";s:3:"㛼";s:4:"㛼";s:3:"嬈";s:4:"嬈";s:3:"嬾";s:4:"嬾";s:4:"𡧈";s:4:"𡧈";s:3:"寃";s:4:"寃";s:3:"寘";s:4:"寘";s:3:"寳";s:4:"寳";s:4:"𡬘";s:4:"𡬘";s:3:"寿";s:4:"寿";s:3:"将";s:4:"将";s:3:"当";s:4:"当";s:3:"尢";s:4:"尢";s:3:"㞁";s:4:"㞁";s:3:"屠";s:4:"屠";s:3:"峀";s:4:"峀";s:3:"岍";s:4:"岍";s:4:"𡷤";s:4:"𡷤";s:3:"嵃";s:4:"嵃";s:4:"𡷦";s:4:"𡷦";s:3:"嵮";s:4:"嵮";s:3:"嵫";s:4:"嵫";s:3:"嵼";s:4:"嵼";s:3:"巡";s:4:"巡";s:3:"巢";s:4:"巢";s:3:"㠯";s:4:"㠯";s:3:"巽";s:4:"巽";s:3:"帨";s:4:"帨";s:3:"帽";s:4:"帽";s:3:"幩";s:4:"幩";s:3:"㡢";s:4:"㡢";s:4:"𢆃";s:4:"𢆃";s:3:"㡼";s:4:"㡼";s:3:"庰";s:4:"庰";s:3:"庳";s:4:"庳";s:3:"庶";s:4:"庶";s:4:"𪎒";s:4:"𪎒";s:3:"廾";s:4:"廾";s:4:"𢌱";s:4:"𢌱";s:3:"舁";s:4:"舁";s:3:"弢";s:4:"弢";s:3:"㣇";s:4:"㣇";s:4:"𣊸";s:4:"𣊸";s:4:"𦇚";s:4:"𦇚";s:3:"形";s:4:"形";s:3:"彫";s:4:"彫";s:3:"㣣";s:4:"㣣";s:3:"徚";s:4:"徚";s:3:"忍";s:4:"忍";s:3:"志";s:4:"志";s:3:"忹";s:4:"忹";s:3:"悁";s:4:"悁";s:3:"㤺";s:4:"㤺";s:3:"㤜";s:4:"㤜";s:4:"𢛔";s:4:"𢛔";s:3:"惇";s:4:"惇";s:3:"慈";s:4:"慈";s:3:"慌";s:4:"慌";s:3:"慺";s:4:"慺";s:3:"憲";s:4:"憲";s:3:"憤";s:4:"憤";s:3:"憯";s:4:"憯";s:3:"懞";s:4:"懞";s:3:"成";s:4:"成";s:3:"戛";s:4:"戛";s:3:"扝";s:4:"扝";s:3:"抱";s:4:"抱";s:3:"拔";s:4:"拔";s:3:"捐";s:4:"捐";s:4:"𢬌";s:4:"𢬌";s:3:"挽";s:4:"挽";s:3:"拼";s:4:"拼";s:3:"捨";s:4:"捨";s:3:"掃";s:4:"掃";s:3:"揤";s:4:"揤";s:4:"𢯱";s:4:"𢯱";s:3:"搢";s:4:"搢";s:3:"揅";s:4:"揅";s:3:"掩";s:4:"掩";s:3:"㨮";s:4:"㨮";s:3:"摩";s:4:"摩";s:3:"摾";s:4:"摾";s:3:"撝";s:4:"撝";s:3:"摷";s:4:"摷";s:3:"㩬";s:4:"㩬";s:3:"敬";s:4:"敬";s:4:"𣀊";s:4:"𣀊";s:3:"旣";s:4:"旣";s:3:"書";s:4:"書";s:3:"晉";s:4:"晉";s:3:"㬙";s:4:"㬙";s:3:"㬈";s:4:"㬈";s:3:"㫤";s:4:"㫤";s:3:"冒";s:4:"冒";s:3:"冕";s:4:"冕";s:3:"最";s:4:"最";s:3:"暜";s:4:"暜";s:3:"肭";s:4:"肭";s:3:"䏙";s:4:"䏙";s:3:"朡";s:4:"朡";s:3:"杞";s:4:"杞";s:3:"杓";s:4:"杓";s:4:"𣏃";s:4:"𣏃";s:3:"㭉";s:4:"㭉";s:3:"柺";s:4:"柺";s:3:"枅";s:4:"枅";s:3:"桒";s:4:"桒";s:4:"𣑭";s:4:"𣑭";s:3:"梎";s:4:"梎";s:3:"栟";s:4:"栟";s:3:"椔";s:4:"椔";s:3:"楂";s:4:"楂";s:3:"榣";s:4:"榣";s:3:"槪";s:4:"槪";s:3:"檨";s:4:"檨";s:4:"𣚣";s:4:"𣚣";s:3:"櫛";s:4:"櫛";s:3:"㰘";s:4:"㰘";s:3:"次";s:4:"次";s:4:"𣢧";s:4:"𣢧";s:3:"歔";s:4:"歔";s:3:"㱎";s:4:"㱎";s:3:"歲";s:4:"歲";s:3:"殟";s:4:"殟";s:3:"殻";s:4:"殻";s:4:"𣪍";s:4:"𣪍";s:4:"𡴋";s:4:"𡴋";s:4:"𣫺";s:4:"𣫺";s:3:"汎";s:4:"汎";s:4:"𣲼";s:4:"𣲼";s:3:"沿";s:4:"沿";s:3:"泍";s:4:"泍";s:3:"汧";s:4:"汧";s:3:"洖";s:4:"洖";s:3:"派";s:4:"派";s:3:"浩";s:4:"浩";s:3:"浸";s:4:"浸";s:3:"涅";s:4:"涅";s:4:"𣴞";s:4:"𣴞";s:3:"洴";s:4:"洴";s:3:"港";s:4:"港";s:3:"湮";s:4:"湮";s:3:"㴳";s:4:"㴳";s:3:"滇";s:4:"滇";s:4:"𣻑";s:4:"𣻑";s:3:"淹";s:4:"淹";s:3:"潮";s:4:"潮";s:4:"𣽞";s:4:"𣽞";s:4:"𣾎";s:4:"𣾎";s:3:"濆";s:4:"濆";s:3:"瀹";s:4:"瀹";s:3:"瀛";s:4:"瀛";s:3:"㶖";s:4:"㶖";s:3:"灊";s:4:"灊";s:3:"災";s:4:"災";s:3:"灷";s:4:"灷";s:3:"炭";s:4:"炭";s:4:"𠔥";s:4:"𠔥";s:3:"煅";s:4:"煅";s:4:"𤉣";s:4:"𤉣";s:3:"熜";s:4:"熜";s:4:"𤎫";s:4:"𤎫";s:3:"爨";s:4:"爨";s:3:"牐";s:4:"牐";s:4:"𤘈";s:4:"𤘈";s:3:"犀";s:4:"犀";s:3:"犕";s:4:"犕";s:4:"𤜵";s:4:"𤜵";s:4:"𤠔";s:4:"𤠔";s:3:"獺";s:4:"獺";s:3:"王";s:4:"王";s:3:"㺬";s:4:"㺬";s:3:"玥";s:4:"玥";s:3:"㺸";s:4:"㺸";s:3:"瑇";s:4:"瑇";s:3:"瑜";s:4:"瑜";s:3:"璅";s:4:"璅";s:3:"瓊";s:4:"瓊";s:3:"㼛";s:4:"㼛";s:3:"甤";s:4:"甤";s:4:"𤰶";s:4:"𤰶";s:3:"甾";s:4:"甾";s:4:"𤲒";s:4:"𤲒";s:4:"𢆟";s:4:"𢆟";s:3:"瘐";s:4:"瘐";s:4:"𤾡";s:4:"𤾡";s:4:"𤾸";s:4:"𤾸";s:4:"𥁄";s:4:"𥁄";s:3:"㿼";s:4:"㿼";s:3:"䀈";s:4:"䀈";s:4:"𥃳";s:4:"𥃳";s:4:"𥃲";s:4:"𥃲";s:4:"𥄙";s:4:"𥄙";s:4:"𥄳";s:4:"𥄳";s:3:"眞";s:4:"眞";s:3:"真";s:4:"真";s:3:"瞋";s:4:"瞋";s:3:"䁆";s:4:"䁆";s:3:"䂖";s:4:"䂖";s:4:"𥐝";s:4:"𥐝";s:3:"硎";s:4:"硎";s:3:"䃣";s:4:"䃣";s:4:"𥘦";s:4:"𥘦";s:4:"𥚚";s:4:"𥚚";s:4:"𥛅";s:4:"𥛅";s:3:"秫";s:4:"秫";s:3:"䄯";s:4:"䄯";s:3:"穊";s:4:"穊";s:3:"穏";s:4:"穏";s:4:"𥥼";s:4:"𥥼";s:4:"𥪧";s:4:"𥪧";s:3:"竮";s:4:"竮";s:3:"䈂";s:4:"䈂";s:4:"𥮫";s:4:"𥮫";s:3:"篆";s:4:"篆";s:3:"築";s:4:"築";s:3:"䈧";s:4:"䈧";s:4:"𥲀";s:4:"𥲀";s:3:"糒";s:4:"糒";s:3:"䊠";s:4:"䊠";s:3:"糨";s:4:"糨";s:3:"糣";s:4:"糣";s:3:"紀";s:4:"紀";s:4:"𥾆";s:4:"𥾆";s:3:"絣";s:4:"絣";s:3:"䌁";s:4:"䌁";s:3:"緇";s:4:"緇";s:3:"縂";s:4:"縂";s:3:"繅";s:4:"繅";s:3:"䌴";s:4:"䌴";s:4:"𦈨";s:4:"𦈨";s:4:"𦉇";s:4:"𦉇";s:3:"䍙";s:4:"䍙";s:4:"𦋙";s:4:"𦋙";s:3:"罺";s:4:"罺";s:4:"𦌾";s:4:"𦌾";s:3:"羕";s:4:"羕";s:3:"翺";s:4:"翺";s:4:"𦓚";s:4:"𦓚";s:4:"𦔣";s:4:"𦔣";s:3:"聠";s:4:"聠";s:4:"𦖨";s:4:"𦖨";s:3:"聰";s:4:"聰";s:4:"𣍟";s:4:"𣍟";s:3:"䏕";s:4:"䏕";s:3:"育";s:4:"育";s:3:"脃";s:4:"脃";s:3:"䐋";s:4:"䐋";s:3:"脾";s:4:"脾";s:3:"媵";s:4:"媵";s:4:"𦞧";s:4:"𦞧";s:4:"𦞵";s:4:"𦞵";s:4:"𣎓";s:4:"𣎓";s:4:"𣎜";s:4:"𣎜";s:3:"舄";s:4:"舄";s:3:"辞";s:4:"辞";s:3:"䑫";s:4:"䑫";s:3:"芑";s:4:"芑";s:3:"芋";s:4:"芋";s:3:"芝";s:4:"芝";s:3:"劳";s:4:"劳";s:3:"花";s:4:"花";s:3:"芳";s:4:"芳";s:3:"芽";s:4:"芽";s:3:"苦";s:4:"苦";s:4:"𦬼";s:4:"𦬼";s:3:"茝";s:4:"茝";s:3:"荣";s:4:"荣";s:3:"莭";s:4:"莭";s:3:"茣";s:4:"茣";s:3:"莽";s:4:"莽";s:3:"菧";s:4:"菧";s:3:"荓";s:4:"荓";s:3:"菊";s:4:"菊";s:3:"菌";s:4:"菌";s:3:"菜";s:4:"菜";s:4:"𦰶";s:4:"𦰶";s:4:"𦵫";s:4:"𦵫";s:4:"𦳕";s:4:"𦳕";s:3:"䔫";s:4:"䔫";s:3:"蓱";s:4:"蓱";s:3:"蓳";s:4:"蓳";s:3:"蔖";s:4:"蔖";s:4:"𧏊";s:4:"𧏊";s:3:"蕤";s:4:"蕤";s:4:"𦼬";s:4:"𦼬";s:3:"䕝";s:4:"䕝";s:3:"䕡";s:4:"䕡";s:4:"𦾱";s:4:"𦾱";s:4:"𧃒";s:4:"𧃒";s:3:"䕫";s:4:"䕫";s:3:"虐";s:4:"虐";s:3:"虧";s:4:"虧";s:3:"虩";s:4:"虩";s:3:"蚩";s:4:"蚩";s:3:"蚈";s:4:"蚈";s:3:"蜎";s:4:"蜎";s:3:"蛢";s:4:"蛢";s:3:"蜨";s:4:"蜨";s:3:"蝫";s:4:"蝫";s:3:"螆";s:4:"螆";s:3:"䗗";s:4:"䗗";s:3:"蟡";s:4:"蟡";s:3:"蠁";s:4:"蠁";s:3:"䗹";s:4:"䗹";s:3:"衠";s:4:"衠";s:3:"衣";s:4:"衣";s:4:"𧙧";s:4:"𧙧";s:3:"裗";s:4:"裗";s:3:"裞";s:4:"裞";s:3:"䘵";s:4:"䘵";s:3:"裺";s:4:"裺";s:3:"㒻";s:4:"㒻";s:4:"𧢮";s:4:"𧢮";s:4:"𧥦";s:4:"𧥦";s:3:"䚾";s:4:"䚾";s:3:"䛇";s:4:"䛇";s:3:"誠";s:4:"誠";s:3:"豕";s:4:"豕";s:4:"𧲨";s:4:"𧲨";s:3:"貫";s:4:"貫";s:3:"賁";s:4:"賁";s:3:"贛";s:4:"贛";s:3:"起";s:4:"起";s:4:"𧼯";s:4:"𧼯";s:4:"𠠄";s:4:"𠠄";s:3:"跋";s:4:"跋";s:3:"趼";s:4:"趼";s:3:"跰";s:4:"跰";s:4:"𠣞";s:4:"𠣞";s:3:"軔";s:4:"軔";s:4:"𨗒";s:4:"𨗒";s:4:"𨗭";s:4:"𨗭";s:3:"邔";s:4:"邔";s:3:"郱";s:4:"郱";s:3:"鄑";s:4:"鄑";s:4:"𨜮";s:4:"𨜮";s:3:"鄛";s:4:"鄛";s:3:"鈸";s:4:"鈸";s:3:"鋗";s:4:"鋗";s:3:"鋘";s:4:"鋘";s:3:"鉼";s:4:"鉼";s:3:"鏹";s:4:"鏹";s:3:"鐕";s:4:"鐕";s:4:"𨯺";s:4:"𨯺";s:3:"開";s:4:"開";s:3:"䦕";s:4:"䦕";s:3:"閷";s:4:"閷";s:4:"𨵷";s:4:"𨵷";s:3:"䧦";s:4:"䧦";s:3:"雃";s:4:"雃";s:3:"嶲";s:4:"嶲";s:3:"霣";s:4:"霣";s:4:"𩅅";s:4:"𩅅";s:4:"𩈚";s:4:"𩈚";s:3:"䩮";s:4:"䩮";s:3:"䩶";s:4:"䩶";s:3:"韠";s:4:"韠";s:4:"𩐊";s:4:"𩐊";s:3:"䪲";s:4:"䪲";s:4:"𩒖";s:4:"𩒖";s:3:"頩";s:4:"頩";s:4:"𩖶";s:4:"𩖶";s:3:"飢";s:4:"飢";s:3:"䬳";s:4:"䬳";s:3:"餩";s:4:"餩";s:3:"馧";s:4:"馧";s:3:"駂";s:4:"駂";s:3:"駾";s:4:"駾";s:3:"䯎";s:4:"䯎";s:4:"𩬰";s:4:"𩬰";s:3:"鱀";s:4:"鱀";s:3:"鳽";s:4:"鳽";s:3:"䳎";s:4:"䳎";s:3:"䳭";s:4:"䳭";s:3:"鵧";s:4:"鵧";s:4:"𪃎";s:4:"𪃎";s:3:"䳸";s:4:"䳸";s:4:"𪄅";s:4:"𪄅";s:4:"𪈎";s:4:"𪈎";s:4:"𪊑";s:4:"𪊑";s:3:"麻";s:4:"麻";s:3:"䵖";s:4:"䵖";s:3:"黹";s:4:"黹";s:3:"黾";s:4:"黾";s:3:"鼅";s:4:"鼅";s:3:"鼏";s:4:"鼏";s:3:"鼖";s:4:"鼖";s:3:"鼻";s:4:"鼻";s:4:"𪘀";s:4:"𪘀";}' );
$utfCanonicalDecomp = unserialize( 'a:2043:{s:2:"À";s:3:"À";s:2:"Á";s:3:"Á";s:2:"Â";s:3:"Â";s:2:"Ã";s:3:"Ã";s:2:"Ä";s:3:"Ä";s:2:"Å";s:3:"Å";s:2:"Ç";s:3:"Ç";s:2:"È";s:3:"È";s:2:"É";s:3:"É";s:2:"Ê";s:3:"Ê";s:2:"Ë";s:3:"Ë";s:2:"Ì";s:3:"Ì";s:2:"Í";s:3:"Í";s:2:"Î";s:3:"Î";s:2:"Ï";s:3:"Ï";s:2:"Ñ";s:3:"Ñ";s:2:"Ò";s:3:"Ò";s:2:"Ó";s:3:"Ó";s:2:"Ô";s:3:"Ô";s:2:"Õ";s:3:"Õ";s:2:"Ö";s:3:"Ö";s:2:"Ù";s:3:"Ù";s:2:"Ú";s:3:"Ú";s:2:"Û";s:3:"Û";s:2:"Ü";s:3:"Ü";s:2:"Ý";s:3:"Ý";s:2:"à";s:3:"à";s:2:"á";s:3:"á";s:2:"â";s:3:"â";s:2:"ã";s:3:"ã";s:2:"ä";s:3:"ä";s:2:"å";s:3:"å";s:2:"ç";s:3:"ç";s:2:"è";s:3:"è";s:2:"é";s:3:"é";s:2:"ê";s:3:"ê";s:2:"ë";s:3:"ë";s:2:"ì";s:3:"ì";s:2:"í";s:3:"í";s:2:"î";s:3:"î";s:2:"ï";s:3:"ï";s:2:"ñ";s:3:"ñ";s:2:"ò";s:3:"ò";s:2:"ó";s:3:"ó";s:2:"ô";s:3:"ô";s:2:"õ";s:3:"õ";s:2:"ö";s:3:"ö";s:2:"ù";s:3:"ù";s:2:"ú";s:3:"ú";s:2:"û";s:3:"û";s:2:"ü";s:3:"ü";s:2:"ý";s:3:"ý";s:2:"ÿ";s:3:"ÿ";s:2:"Ā";s:3:"Ā";s:2:"ā";s:3:"ā";s:2:"Ă";s:3:"Ă";s:2:"ă";s:3:"ă";s:2:"Ą";s:3:"Ą";s:2:"ą";s:3:"ą";s:2:"Ć";s:3:"Ć";s:2:"ć";s:3:"ć";s:2:"Ĉ";s:3:"Ĉ";s:2:"ĉ";s:3:"ĉ";s:2:"Ċ";s:3:"Ċ";s:2:"ċ";s:3:"ċ";s:2:"Č";s:3:"Č";s:2:"č";s:3:"č";s:2:"Ď";s:3:"Ď";s:2:"ď";s:3:"ď";s:2:"Ē";s:3:"Ē";s:2:"ē";s:3:"ē";s:2:"Ĕ";s:3:"Ĕ";s:2:"ĕ";s:3:"ĕ";s:2:"Ė";s:3:"Ė";s:2:"ė";s:3:"ė";s:2:"Ę";s:3:"Ę";s:2:"ę";s:3:"ę";s:2:"Ě";s:3:"Ě";s:2:"ě";s:3:"ě";s:2:"Ĝ";s:3:"Ĝ";s:2:"ĝ";s:3:"ĝ";s:2:"Ğ";s:3:"Ğ";s:2:"ğ";s:3:"ğ";s:2:"Ġ";s:3:"Ġ";s:2:"ġ";s:3:"ġ";s:2:"Ģ";s:3:"Ģ";s:2:"ģ";s:3:"ģ";s:2:"Ĥ";s:3:"Ĥ";s:2:"ĥ";s:3:"ĥ";s:2:"Ĩ";s:3:"Ĩ";s:2:"ĩ";s:3:"ĩ";s:2:"Ī";s:3:"Ī";s:2:"ī";s:3:"ī";s:2:"Ĭ";s:3:"Ĭ";s:2:"ĭ";s:3:"ĭ";s:2:"Į";s:3:"Į";s:2:"į";s:3:"į";s:2:"İ";s:3:"İ";s:2:"Ĵ";s:3:"Ĵ";s:2:"ĵ";s:3:"ĵ";s:2:"Ķ";s:3:"Ķ";s:2:"ķ";s:3:"ķ";s:2:"Ĺ";s:3:"Ĺ";s:2:"ĺ";s:3:"ĺ";s:2:"Ļ";s:3:"Ļ";s:2:"ļ";s:3:"ļ";s:2:"Ľ";s:3:"Ľ";s:2:"ľ";s:3:"ľ";s:2:"Ń";s:3:"Ń";s:2:"ń";s:3:"ń";s:2:"Ņ";s:3:"Ņ";s:2:"ņ";s:3:"ņ";s:2:"Ň";s:3:"Ň";s:2:"ň";s:3:"ň";s:2:"Ō";s:3:"Ō";s:2:"ō";s:3:"ō";s:2:"Ŏ";s:3:"Ŏ";s:2:"ŏ";s:3:"ŏ";s:2:"Ő";s:3:"Ő";s:2:"ő";s:3:"ő";s:2:"Ŕ";s:3:"Ŕ";s:2:"ŕ";s:3:"ŕ";s:2:"Ŗ";s:3:"Ŗ";s:2:"ŗ";s:3:"ŗ";s:2:"Ř";s:3:"Ř";s:2:"ř";s:3:"ř";s:2:"Ś";s:3:"Ś";s:2:"ś";s:3:"ś";s:2:"Ŝ";s:3:"Ŝ";s:2:"ŝ";s:3:"ŝ";s:2:"Ş";s:3:"Ş";s:2:"ş";s:3:"ş";s:2:"Š";s:3:"Š";s:2:"š";s:3:"š";s:2:"Ţ";s:3:"Ţ";s:2:"ţ";s:3:"ţ";s:2:"Ť";s:3:"Ť";s:2:"ť";s:3:"ť";s:2:"Ũ";s:3:"Ũ";s:2:"ũ";s:3:"ũ";s:2:"Ū";s:3:"Ū";s:2:"ū";s:3:"ū";s:2:"Ŭ";s:3:"Ŭ";s:2:"ŭ";s:3:"ŭ";s:2:"Ů";s:3:"Ů";s:2:"ů";s:3:"ů";s:2:"Ű";s:3:"Ű";s:2:"ű";s:3:"ű";s:2:"Ų";s:3:"Ų";s:2:"ų";s:3:"ų";s:2:"Ŵ";s:3:"Ŵ";s:2:"ŵ";s:3:"ŵ";s:2:"Ŷ";s:3:"Ŷ";s:2:"ŷ";s:3:"ŷ";s:2:"Ÿ";s:3:"Ÿ";s:2:"Ź";s:3:"Ź";s:2:"ź";s:3:"ź";s:2:"Ż";s:3:"Ż";s:2:"ż";s:3:"ż";s:2:"Ž";s:3:"Ž";s:2:"ž";s:3:"ž";s:2:"Ơ";s:3:"Ơ";s:2:"ơ";s:3:"ơ";s:2:"Ư";s:3:"Ư";s:2:"ư";s:3:"ư";s:2:"Ǎ";s:3:"Ǎ";s:2:"ǎ";s:3:"ǎ";s:2:"Ǐ";s:3:"Ǐ";s:2:"ǐ";s:3:"ǐ";s:2:"Ǒ";s:3:"Ǒ";s:2:"ǒ";s:3:"ǒ";s:2:"Ǔ";s:3:"Ǔ";s:2:"ǔ";s:3:"ǔ";s:2:"Ǖ";s:5:"Ǖ";s:2:"ǖ";s:5:"ǖ";s:2:"Ǘ";s:5:"Ǘ";s:2:"ǘ";s:5:"ǘ";s:2:"Ǚ";s:5:"Ǚ";s:2:"ǚ";s:5:"ǚ";s:2:"Ǜ";s:5:"Ǜ";s:2:"ǜ";s:5:"ǜ";s:2:"Ǟ";s:5:"Ǟ";s:2:"ǟ";s:5:"ǟ";s:2:"Ǡ";s:5:"Ǡ";s:2:"ǡ";s:5:"ǡ";s:2:"Ǣ";s:4:"Ǣ";s:2:"ǣ";s:4:"ǣ";s:2:"Ǧ";s:3:"Ǧ";s:2:"ǧ";s:3:"ǧ";s:2:"Ǩ";s:3:"Ǩ";s:2:"ǩ";s:3:"ǩ";s:2:"Ǫ";s:3:"Ǫ";s:2:"ǫ";s:3:"ǫ";s:2:"Ǭ";s:5:"Ǭ";s:2:"ǭ";s:5:"ǭ";s:2:"Ǯ";s:4:"Ǯ";s:2:"ǯ";s:4:"ǯ";s:2:"ǰ";s:3:"ǰ";s:2:"Ǵ";s:3:"Ǵ";s:2:"ǵ";s:3:"ǵ";s:2:"Ǹ";s:3:"Ǹ";s:2:"ǹ";s:3:"ǹ";s:2:"Ǻ";s:5:"Ǻ";s:2:"ǻ";s:5:"ǻ";s:2:"Ǽ";s:4:"Ǽ";s:2:"ǽ";s:4:"ǽ";s:2:"Ǿ";s:4:"Ǿ";s:2:"ǿ";s:4:"ǿ";s:2:"Ȁ";s:3:"Ȁ";s:2:"ȁ";s:3:"ȁ";s:2:"Ȃ";s:3:"Ȃ";s:2:"ȃ";s:3:"ȃ";s:2:"Ȅ";s:3:"Ȅ";s:2:"ȅ";s:3:"ȅ";s:2:"Ȇ";s:3:"Ȇ";s:2:"ȇ";s:3:"ȇ";s:2:"Ȉ";s:3:"Ȉ";s:2:"ȉ";s:3:"ȉ";s:2:"Ȋ";s:3:"Ȋ";s:2:"ȋ";s:3:"ȋ";s:2:"Ȍ";s:3:"Ȍ";s:2:"ȍ";s:3:"ȍ";s:2:"Ȏ";s:3:"Ȏ";s:2:"ȏ";s:3:"ȏ";s:2:"Ȑ";s:3:"Ȑ";s:2:"ȑ";s:3:"ȑ";s:2:"Ȓ";s:3:"Ȓ";s:2:"ȓ";s:3:"ȓ";s:2:"Ȕ";s:3:"Ȕ";s:2:"ȕ";s:3:"ȕ";s:2:"Ȗ";s:3:"Ȗ";s:2:"ȗ";s:3:"ȗ";s:2:"Ș";s:3:"Ș";s:2:"ș";s:3:"ș";s:2:"Ț";s:3:"Ț";s:2:"ț";s:3:"ț";s:2:"Ȟ";s:3:"Ȟ";s:2:"ȟ";s:3:"ȟ";s:2:"Ȧ";s:3:"Ȧ";s:2:"ȧ";s:3:"ȧ";s:2:"Ȩ";s:3:"Ȩ";s:2:"ȩ";s:3:"ȩ";s:2:"Ȫ";s:5:"Ȫ";s:2:"ȫ";s:5:"ȫ";s:2:"Ȭ";s:5:"Ȭ";s:2:"ȭ";s:5:"ȭ";s:2:"Ȯ";s:3:"Ȯ";s:2:"ȯ";s:3:"ȯ";s:2:"Ȱ";s:5:"Ȱ";s:2:"ȱ";s:5:"ȱ";s:2:"Ȳ";s:3:"Ȳ";s:2:"ȳ";s:3:"ȳ";s:2:"̀";s:2:"̀";s:2:"́";s:2:"́";s:2:"̓";s:2:"̓";s:2:"̈́";s:4:"̈́";s:2:"ʹ";s:2:"ʹ";s:2:";";s:1:";";s:2:"΅";s:4:"΅";s:2:"Ά";s:4:"Ά";s:2:"·";s:2:"·";s:2:"Έ";s:4:"Έ";s:2:"Ή";s:4:"Ή";s:2:"Ί";s:4:"Ί";s:2:"Ό";s:4:"Ό";s:2:"Ύ";s:4:"Ύ";s:2:"Ώ";s:4:"Ώ";s:2:"ΐ";s:6:"ΐ";s:2:"Ϊ";s:4:"Ϊ";s:2:"Ϋ";s:4:"Ϋ";s:2:"ά";s:4:"ά";s:2:"έ";s:4:"έ";s:2:"ή";s:4:"ή";s:2:"ί";s:4:"ί";s:2:"ΰ";s:6:"ΰ";s:2:"ϊ";s:4:"ϊ";s:2:"ϋ";s:4:"ϋ";s:2:"ό";s:4:"ό";s:2:"ύ";s:4:"ύ";s:2:"ώ";s:4:"ώ";s:2:"ϓ";s:4:"ϓ";s:2:"ϔ";s:4:"ϔ";s:2:"Ѐ";s:4:"Ѐ";s:2:"Ё";s:4:"Ё";s:2:"Ѓ";s:4:"Ѓ";s:2:"Ї";s:4:"Ї";s:2:"Ќ";s:4:"Ќ";s:2:"Ѝ";s:4:"Ѝ";s:2:"Ў";s:4:"Ў";s:2:"Й";s:4:"Й";s:2:"й";s:4:"й";s:2:"ѐ";s:4:"ѐ";s:2:"ё";s:4:"ё";s:2:"ѓ";s:4:"ѓ";s:2:"ї";s:4:"ї";s:2:"ќ";s:4:"ќ";s:2:"ѝ";s:4:"ѝ";s:2:"ў";s:4:"ў";s:2:"Ѷ";s:4:"Ѷ";s:2:"ѷ";s:4:"ѷ";s:2:"Ӂ";s:4:"Ӂ";s:2:"ӂ";s:4:"ӂ";s:2:"Ӑ";s:4:"Ӑ";s:2:"ӑ";s:4:"ӑ";s:2:"Ӓ";s:4:"Ӓ";s:2:"ӓ";s:4:"ӓ";s:2:"Ӗ";s:4:"Ӗ";s:2:"ӗ";s:4:"ӗ";s:2:"Ӛ";s:4:"Ӛ";s:2:"ӛ";s:4:"ӛ";s:2:"Ӝ";s:4:"Ӝ";s:2:"ӝ";s:4:"ӝ";s:2:"Ӟ";s:4:"Ӟ";s:2:"ӟ";s:4:"ӟ";s:2:"Ӣ";s:4:"Ӣ";s:2:"ӣ";s:4:"ӣ";s:2:"Ӥ";s:4:"Ӥ";s:2:"ӥ";s:4:"ӥ";s:2:"Ӧ";s:4:"Ӧ";s:2:"ӧ";s:4:"ӧ";s:2:"Ӫ";s:4:"Ӫ";s:2:"ӫ";s:4:"ӫ";s:2:"Ӭ";s:4:"Ӭ";s:2:"ӭ";s:4:"ӭ";s:2:"Ӯ";s:4:"Ӯ";s:2:"ӯ";s:4:"ӯ";s:2:"Ӱ";s:4:"Ӱ";s:2:"ӱ";s:4:"ӱ";s:2:"Ӳ";s:4:"Ӳ";s:2:"ӳ";s:4:"ӳ";s:2:"Ӵ";s:4:"Ӵ";s:2:"ӵ";s:4:"ӵ";s:2:"Ӹ";s:4:"Ӹ";s:2:"ӹ";s:4:"ӹ";s:2:"آ";s:4:"آ";s:2:"أ";s:4:"أ";s:2:"ؤ";s:4:"ؤ";s:2:"إ";s:4:"إ";s:2:"ئ";s:4:"ئ";s:2:"ۀ";s:4:"ۀ";s:2:"ۂ";s:4:"ۂ";s:2:"ۓ";s:4:"ۓ";s:3:"ऩ";s:6:"ऩ";s:3:"ऱ";s:6:"ऱ";s:3:"ऴ";s:6:"ऴ";s:3:"क़";s:6:"क़";s:3:"ख़";s:6:"ख़";s:3:"ग़";s:6:"ग़";s:3:"ज़";s:6:"ज़";s:3:"ड़";s:6:"ड़";s:3:"ढ़";s:6:"ढ़";s:3:"फ़";s:6:"फ़";s:3:"य़";s:6:"य़";s:3:"ো";s:6:"ো";s:3:"ৌ";s:6:"ৌ";s:3:"ড়";s:6:"ড়";s:3:"ঢ়";s:6:"ঢ়";s:3:"য়";s:6:"য়";s:3:"ਲ਼";s:6:"ਲ਼";s:3:"ਸ਼";s:6:"ਸ਼";s:3:"ਖ਼";s:6:"ਖ਼";s:3:"ਗ਼";s:6:"ਗ਼";s:3:"ਜ਼";s:6:"ਜ਼";s:3:"ਫ਼";s:6:"ਫ਼";s:3:"ୈ";s:6:"ୈ";s:3:"ୋ";s:6:"ୋ";s:3:"ୌ";s:6:"ୌ";s:3:"ଡ଼";s:6:"ଡ଼";s:3:"ଢ଼";s:6:"ଢ଼";s:3:"ஔ";s:6:"ஔ";s:3:"ொ";s:6:"ொ";s:3:"ோ";s:6:"ோ";s:3:"ௌ";s:6:"ௌ";s:3:"ై";s:6:"ై";s:3:"ೀ";s:6:"ೀ";s:3:"ೇ";s:6:"ೇ";s:3:"ೈ";s:6:"ೈ";s:3:"ೊ";s:6:"ೊ";s:3:"ೋ";s:9:"ೋ";s:3:"ൊ";s:6:"ൊ";s:3:"ോ";s:6:"ോ";s:3:"ൌ";s:6:"ൌ";s:3:"ේ";s:6:"ේ";s:3:"ො";s:6:"ො";s:3:"ෝ";s:9:"ෝ";s:3:"ෞ";s:6:"ෞ";s:3:"གྷ";s:6:"གྷ";s:3:"ཌྷ";s:6:"ཌྷ";s:3:"དྷ";s:6:"དྷ";s:3:"བྷ";s:6:"བྷ";s:3:"ཛྷ";s:6:"ཛྷ";s:3:"ཀྵ";s:6:"ཀྵ";s:3:"ཱི";s:6:"ཱི";s:3:"ཱུ";s:6:"ཱུ";s:3:"ྲྀ";s:6:"ྲྀ";s:3:"ླྀ";s:6:"ླྀ";s:3:"ཱྀ";s:6:"ཱྀ";s:3:"ྒྷ";s:6:"ྒྷ";s:3:"ྜྷ";s:6:"ྜྷ";s:3:"ྡྷ";s:6:"ྡྷ";s:3:"ྦྷ";s:6:"ྦྷ";s:3:"ྫྷ";s:6:"ྫྷ";s:3:"ྐྵ";s:6:"ྐྵ";s:3:"ဦ";s:6:"ဦ";s:3:"ᬆ";s:6:"ᬆ";s:3:"ᬈ";s:6:"ᬈ";s:3:"ᬊ";s:6:"ᬊ";s:3:"ᬌ";s:6:"ᬌ";s:3:"ᬎ";s:6:"ᬎ";s:3:"ᬒ";s:6:"ᬒ";s:3:"ᬻ";s:6:"ᬻ";s:3:"ᬽ";s:6:"ᬽ";s:3:"ᭀ";s:6:"ᭀ";s:3:"ᭁ";s:6:"ᭁ";s:3:"ᭃ";s:6:"ᭃ";s:3:"Ḁ";s:3:"Ḁ";s:3:"ḁ";s:3:"ḁ";s:3:"Ḃ";s:3:"Ḃ";s:3:"ḃ";s:3:"ḃ";s:3:"Ḅ";s:3:"Ḅ";s:3:"ḅ";s:3:"ḅ";s:3:"Ḇ";s:3:"Ḇ";s:3:"ḇ";s:3:"ḇ";s:3:"Ḉ";s:5:"Ḉ";s:3:"ḉ";s:5:"ḉ";s:3:"Ḋ";s:3:"Ḋ";s:3:"ḋ";s:3:"ḋ";s:3:"Ḍ";s:3:"Ḍ";s:3:"ḍ";s:3:"ḍ";s:3:"Ḏ";s:3:"Ḏ";s:3:"ḏ";s:3:"ḏ";s:3:"Ḑ";s:3:"Ḑ";s:3:"ḑ";s:3:"ḑ";s:3:"Ḓ";s:3:"Ḓ";s:3:"ḓ";s:3:"ḓ";s:3:"Ḕ";s:5:"Ḕ";s:3:"ḕ";s:5:"ḕ";s:3:"Ḗ";s:5:"Ḗ";s:3:"ḗ";s:5:"ḗ";s:3:"Ḙ";s:3:"Ḙ";s:3:"ḙ";s:3:"ḙ";s:3:"Ḛ";s:3:"Ḛ";s:3:"ḛ";s:3:"ḛ";s:3:"Ḝ";s:5:"Ḝ";s:3:"ḝ";s:5:"ḝ";s:3:"Ḟ";s:3:"Ḟ";s:3:"ḟ";s:3:"ḟ";s:3:"Ḡ";s:3:"Ḡ";s:3:"ḡ";s:3:"ḡ";s:3:"Ḣ";s:3:"Ḣ";s:3:"ḣ";s:3:"ḣ";s:3:"Ḥ";s:3:"Ḥ";s:3:"ḥ";s:3:"ḥ";s:3:"Ḧ";s:3:"Ḧ";s:3:"ḧ";s:3:"ḧ";s:3:"Ḩ";s:3:"Ḩ";s:3:"ḩ";s:3:"ḩ";s:3:"Ḫ";s:3:"Ḫ";s:3:"ḫ";s:3:"ḫ";s:3:"Ḭ";s:3:"Ḭ";s:3:"ḭ";s:3:"ḭ";s:3:"Ḯ";s:5:"Ḯ";s:3:"ḯ";s:5:"ḯ";s:3:"Ḱ";s:3:"Ḱ";s:3:"ḱ";s:3:"ḱ";s:3:"Ḳ";s:3:"Ḳ";s:3:"ḳ";s:3:"ḳ";s:3:"Ḵ";s:3:"Ḵ";s:3:"ḵ";s:3:"ḵ";s:3:"Ḷ";s:3:"Ḷ";s:3:"ḷ";s:3:"ḷ";s:3:"Ḹ";s:5:"Ḹ";s:3:"ḹ";s:5:"ḹ";s:3:"Ḻ";s:3:"Ḻ";s:3:"ḻ";s:3:"ḻ";s:3:"Ḽ";s:3:"Ḽ";s:3:"ḽ";s:3:"ḽ";s:3:"Ḿ";s:3:"Ḿ";s:3:"ḿ";s:3:"ḿ";s:3:"Ṁ";s:3:"Ṁ";s:3:"ṁ";s:3:"ṁ";s:3:"Ṃ";s:3:"Ṃ";s:3:"ṃ";s:3:"ṃ";s:3:"Ṅ";s:3:"Ṅ";s:3:"ṅ";s:3:"ṅ";s:3:"Ṇ";s:3:"Ṇ";s:3:"ṇ";s:3:"ṇ";s:3:"Ṉ";s:3:"Ṉ";s:3:"ṉ";s:3:"ṉ";s:3:"Ṋ";s:3:"Ṋ";s:3:"ṋ";s:3:"ṋ";s:3:"Ṍ";s:5:"Ṍ";s:3:"ṍ";s:5:"ṍ";s:3:"Ṏ";s:5:"Ṏ";s:3:"ṏ";s:5:"ṏ";s:3:"Ṑ";s:5:"Ṑ";s:3:"ṑ";s:5:"ṑ";s:3:"Ṓ";s:5:"Ṓ";s:3:"ṓ";s:5:"ṓ";s:3:"Ṕ";s:3:"Ṕ";s:3:"ṕ";s:3:"ṕ";s:3:"Ṗ";s:3:"Ṗ";s:3:"ṗ";s:3:"ṗ";s:3:"Ṙ";s:3:"Ṙ";s:3:"ṙ";s:3:"ṙ";s:3:"Ṛ";s:3:"Ṛ";s:3:"ṛ";s:3:"ṛ";s:3:"Ṝ";s:5:"Ṝ";s:3:"ṝ";s:5:"ṝ";s:3:"Ṟ";s:3:"Ṟ";s:3:"ṟ";s:3:"ṟ";s:3:"Ṡ";s:3:"Ṡ";s:3:"ṡ";s:3:"ṡ";s:3:"Ṣ";s:3:"Ṣ";s:3:"ṣ";s:3:"ṣ";s:3:"Ṥ";s:5:"Ṥ";s:3:"ṥ";s:5:"ṥ";s:3:"Ṧ";s:5:"Ṧ";s:3:"ṧ";s:5:"ṧ";s:3:"Ṩ";s:5:"Ṩ";s:3:"ṩ";s:5:"ṩ";s:3:"Ṫ";s:3:"Ṫ";s:3:"ṫ";s:3:"ṫ";s:3:"Ṭ";s:3:"Ṭ";s:3:"ṭ";s:3:"ṭ";s:3:"Ṯ";s:3:"Ṯ";s:3:"ṯ";s:3:"ṯ";s:3:"Ṱ";s:3:"Ṱ";s:3:"ṱ";s:3:"ṱ";s:3:"Ṳ";s:3:"Ṳ";s:3:"ṳ";s:3:"ṳ";s:3:"Ṵ";s:3:"Ṵ";s:3:"ṵ";s:3:"ṵ";s:3:"Ṷ";s:3:"Ṷ";s:3:"ṷ";s:3:"ṷ";s:3:"Ṹ";s:5:"Ṹ";s:3:"ṹ";s:5:"ṹ";s:3:"Ṻ";s:5:"Ṻ";s:3:"ṻ";s:5:"ṻ";s:3:"Ṽ";s:3:"Ṽ";s:3:"ṽ";s:3:"ṽ";s:3:"Ṿ";s:3:"Ṿ";s:3:"ṿ";s:3:"ṿ";s:3:"Ẁ";s:3:"Ẁ";s:3:"ẁ";s:3:"ẁ";s:3:"Ẃ";s:3:"Ẃ";s:3:"ẃ";s:3:"ẃ";s:3:"Ẅ";s:3:"Ẅ";s:3:"ẅ";s:3:"ẅ";s:3:"Ẇ";s:3:"Ẇ";s:3:"ẇ";s:3:"ẇ";s:3:"Ẉ";s:3:"Ẉ";s:3:"ẉ";s:3:"ẉ";s:3:"Ẋ";s:3:"Ẋ";s:3:"ẋ";s:3:"ẋ";s:3:"Ẍ";s:3:"Ẍ";s:3:"ẍ";s:3:"ẍ";s:3:"Ẏ";s:3:"Ẏ";s:3:"ẏ";s:3:"ẏ";s:3:"Ẑ";s:3:"Ẑ";s:3:"ẑ";s:3:"ẑ";s:3:"Ẓ";s:3:"Ẓ";s:3:"ẓ";s:3:"ẓ";s:3:"Ẕ";s:3:"Ẕ";s:3:"ẕ";s:3:"ẕ";s:3:"ẖ";s:3:"ẖ";s:3:"ẗ";s:3:"ẗ";s:3:"ẘ";s:3:"ẘ";s:3:"ẙ";s:3:"ẙ";s:3:"ẛ";s:4:"ẛ";s:3:"Ạ";s:3:"Ạ";s:3:"ạ";s:3:"ạ";s:3:"Ả";s:3:"Ả";s:3:"ả";s:3:"ả";s:3:"Ấ";s:5:"Ấ";s:3:"ấ";s:5:"ấ";s:3:"Ầ";s:5:"Ầ";s:3:"ầ";s:5:"ầ";s:3:"Ẩ";s:5:"Ẩ";s:3:"ẩ";s:5:"ẩ";s:3:"Ẫ";s:5:"Ẫ";s:3:"ẫ";s:5:"ẫ";s:3:"Ậ";s:5:"Ậ";s:3:"ậ";s:5:"ậ";s:3:"Ắ";s:5:"Ắ";s:3:"ắ";s:5:"ắ";s:3:"Ằ";s:5:"Ằ";s:3:"ằ";s:5:"ằ";s:3:"Ẳ";s:5:"Ẳ";s:3:"ẳ";s:5:"ẳ";s:3:"Ẵ";s:5:"Ẵ";s:3:"ẵ";s:5:"ẵ";s:3:"Ặ";s:5:"Ặ";s:3:"ặ";s:5:"ặ";s:3:"Ẹ";s:3:"Ẹ";s:3:"ẹ";s:3:"ẹ";s:3:"Ẻ";s:3:"Ẻ";s:3:"ẻ";s:3:"ẻ";s:3:"Ẽ";s:3:"Ẽ";s:3:"ẽ";s:3:"ẽ";s:3:"Ế";s:5:"Ế";s:3:"ế";s:5:"ế";s:3:"Ề";s:5:"Ề";s:3:"ề";s:5:"ề";s:3:"Ể";s:5:"Ể";s:3:"ể";s:5:"ể";s:3:"Ễ";s:5:"Ễ";s:3:"ễ";s:5:"ễ";s:3:"Ệ";s:5:"Ệ";s:3:"ệ";s:5:"ệ";s:3:"Ỉ";s:3:"Ỉ";s:3:"ỉ";s:3:"ỉ";s:3:"Ị";s:3:"Ị";s:3:"ị";s:3:"ị";s:3:"Ọ";s:3:"Ọ";s:3:"ọ";s:3:"ọ";s:3:"Ỏ";s:3:"Ỏ";s:3:"ỏ";s:3:"ỏ";s:3:"Ố";s:5:"Ố";s:3:"ố";s:5:"ố";s:3:"Ồ";s:5:"Ồ";s:3:"ồ";s:5:"ồ";s:3:"Ổ";s:5:"Ổ";s:3:"ổ";s:5:"ổ";s:3:"Ỗ";s:5:"Ỗ";s:3:"ỗ";s:5:"ỗ";s:3:"Ộ";s:5:"Ộ";s:3:"ộ";s:5:"ộ";s:3:"Ớ";s:5:"Ớ";s:3:"ớ";s:5:"ớ";s:3:"Ờ";s:5:"Ờ";s:3:"ờ";s:5:"ờ";s:3:"Ở";s:5:"Ở";s:3:"ở";s:5:"ở";s:3:"Ỡ";s:5:"Ỡ";s:3:"ỡ";s:5:"ỡ";s:3:"Ợ";s:5:"Ợ";s:3:"ợ";s:5:"ợ";s:3:"Ụ";s:3:"Ụ";s:3:"ụ";s:3:"ụ";s:3:"Ủ";s:3:"Ủ";s:3:"ủ";s:3:"ủ";s:3:"Ứ";s:5:"Ứ";s:3:"ứ";s:5:"ứ";s:3:"Ừ";s:5:"Ừ";s:3:"ừ";s:5:"ừ";s:3:"Ử";s:5:"Ử";s:3:"ử";s:5:"ử";s:3:"Ữ";s:5:"Ữ";s:3:"ữ";s:5:"ữ";s:3:"Ự";s:5:"Ự";s:3:"ự";s:5:"ự";s:3:"Ỳ";s:3:"Ỳ";s:3:"ỳ";s:3:"ỳ";s:3:"Ỵ";s:3:"Ỵ";s:3:"ỵ";s:3:"ỵ";s:3:"Ỷ";s:3:"Ỷ";s:3:"ỷ";s:3:"ỷ";s:3:"Ỹ";s:3:"Ỹ";s:3:"ỹ";s:3:"ỹ";s:3:"ἀ";s:4:"ἀ";s:3:"ἁ";s:4:"ἁ";s:3:"ἂ";s:6:"ἂ";s:3:"ἃ";s:6:"ἃ";s:3:"ἄ";s:6:"ἄ";s:3:"ἅ";s:6:"ἅ";s:3:"ἆ";s:6:"ἆ";s:3:"ἇ";s:6:"ἇ";s:3:"Ἀ";s:4:"Ἀ";s:3:"Ἁ";s:4:"Ἁ";s:3:"Ἂ";s:6:"Ἂ";s:3:"Ἃ";s:6:"Ἃ";s:3:"Ἄ";s:6:"Ἄ";s:3:"Ἅ";s:6:"Ἅ";s:3:"Ἆ";s:6:"Ἆ";s:3:"Ἇ";s:6:"Ἇ";s:3:"ἐ";s:4:"ἐ";s:3:"ἑ";s:4:"ἑ";s:3:"ἒ";s:6:"ἒ";s:3:"ἓ";s:6:"ἓ";s:3:"ἔ";s:6:"ἔ";s:3:"ἕ";s:6:"ἕ";s:3:"Ἐ";s:4:"Ἐ";s:3:"Ἑ";s:4:"Ἑ";s:3:"Ἒ";s:6:"Ἒ";s:3:"Ἓ";s:6:"Ἓ";s:3:"Ἔ";s:6:"Ἔ";s:3:"Ἕ";s:6:"Ἕ";s:3:"ἠ";s:4:"ἠ";s:3:"ἡ";s:4:"ἡ";s:3:"ἢ";s:6:"ἢ";s:3:"ἣ";s:6:"ἣ";s:3:"ἤ";s:6:"ἤ";s:3:"ἥ";s:6:"ἥ";s:3:"ἦ";s:6:"ἦ";s:3:"ἧ";s:6:"ἧ";s:3:"Ἠ";s:4:"Ἠ";s:3:"Ἡ";s:4:"Ἡ";s:3:"Ἢ";s:6:"Ἢ";s:3:"Ἣ";s:6:"Ἣ";s:3:"Ἤ";s:6:"Ἤ";s:3:"Ἥ";s:6:"Ἥ";s:3:"Ἦ";s:6:"Ἦ";s:3:"Ἧ";s:6:"Ἧ";s:3:"ἰ";s:4:"ἰ";s:3:"ἱ";s:4:"ἱ";s:3:"ἲ";s:6:"ἲ";s:3:"ἳ";s:6:"ἳ";s:3:"ἴ";s:6:"ἴ";s:3:"ἵ";s:6:"ἵ";s:3:"ἶ";s:6:"ἶ";s:3:"ἷ";s:6:"ἷ";s:3:"Ἰ";s:4:"Ἰ";s:3:"Ἱ";s:4:"Ἱ";s:3:"Ἲ";s:6:"Ἲ";s:3:"Ἳ";s:6:"Ἳ";s:3:"Ἴ";s:6:"Ἴ";s:3:"Ἵ";s:6:"Ἵ";s:3:"Ἶ";s:6:"Ἶ";s:3:"Ἷ";s:6:"Ἷ";s:3:"ὀ";s:4:"ὀ";s:3:"ὁ";s:4:"ὁ";s:3:"ὂ";s:6:"ὂ";s:3:"ὃ";s:6:"ὃ";s:3:"ὄ";s:6:"ὄ";s:3:"ὅ";s:6:"ὅ";s:3:"Ὀ";s:4:"Ὀ";s:3:"Ὁ";s:4:"Ὁ";s:3:"Ὂ";s:6:"Ὂ";s:3:"Ὃ";s:6:"Ὃ";s:3:"Ὄ";s:6:"Ὄ";s:3:"Ὅ";s:6:"Ὅ";s:3:"ὐ";s:4:"ὐ";s:3:"ὑ";s:4:"ὑ";s:3:"ὒ";s:6:"ὒ";s:3:"ὓ";s:6:"ὓ";s:3:"ὔ";s:6:"ὔ";s:3:"ὕ";s:6:"ὕ";s:3:"ὖ";s:6:"ὖ";s:3:"ὗ";s:6:"ὗ";s:3:"Ὑ";s:4:"Ὑ";s:3:"Ὓ";s:6:"Ὓ";s:3:"Ὕ";s:6:"Ὕ";s:3:"Ὗ";s:6:"Ὗ";s:3:"ὠ";s:4:"ὠ";s:3:"ὡ";s:4:"ὡ";s:3:"ὢ";s:6:"ὢ";s:3:"ὣ";s:6:"ὣ";s:3:"ὤ";s:6:"ὤ";s:3:"ὥ";s:6:"ὥ";s:3:"ὦ";s:6:"ὦ";s:3:"ὧ";s:6:"ὧ";s:3:"Ὠ";s:4:"Ὠ";s:3:"Ὡ";s:4:"Ὡ";s:3:"Ὢ";s:6:"Ὢ";s:3:"Ὣ";s:6:"Ὣ";s:3:"Ὤ";s:6:"Ὤ";s:3:"Ὥ";s:6:"Ὥ";s:3:"Ὦ";s:6:"Ὦ";s:3:"Ὧ";s:6:"Ὧ";s:3:"ὰ";s:4:"ὰ";s:3:"ά";s:4:"ά";s:3:"ὲ";s:4:"ὲ";s:3:"έ";s:4:"έ";s:3:"ὴ";s:4:"ὴ";s:3:"ή";s:4:"ή";s:3:"ὶ";s:4:"ὶ";s:3:"ί";s:4:"ί";s:3:"ὸ";s:4:"ὸ";s:3:"ό";s:4:"ό";s:3:"ὺ";s:4:"ὺ";s:3:"ύ";s:4:"ύ";s:3:"ὼ";s:4:"ὼ";s:3:"ώ";s:4:"ώ";s:3:"ᾀ";s:6:"ᾀ";s:3:"ᾁ";s:6:"ᾁ";s:3:"ᾂ";s:8:"ᾂ";s:3:"ᾃ";s:8:"ᾃ";s:3:"ᾄ";s:8:"ᾄ";s:3:"ᾅ";s:8:"ᾅ";s:3:"ᾆ";s:8:"ᾆ";s:3:"ᾇ";s:8:"ᾇ";s:3:"ᾈ";s:6:"ᾈ";s:3:"ᾉ";s:6:"ᾉ";s:3:"ᾊ";s:8:"ᾊ";s:3:"ᾋ";s:8:"ᾋ";s:3:"ᾌ";s:8:"ᾌ";s:3:"ᾍ";s:8:"ᾍ";s:3:"ᾎ";s:8:"ᾎ";s:3:"ᾏ";s:8:"ᾏ";s:3:"ᾐ";s:6:"ᾐ";s:3:"ᾑ";s:6:"ᾑ";s:3:"ᾒ";s:8:"ᾒ";s:3:"ᾓ";s:8:"ᾓ";s:3:"ᾔ";s:8:"ᾔ";s:3:"ᾕ";s:8:"ᾕ";s:3:"ᾖ";s:8:"ᾖ";s:3:"ᾗ";s:8:"ᾗ";s:3:"ᾘ";s:6:"ᾘ";s:3:"ᾙ";s:6:"ᾙ";s:3:"ᾚ";s:8:"ᾚ";s:3:"ᾛ";s:8:"ᾛ";s:3:"ᾜ";s:8:"ᾜ";s:3:"ᾝ";s:8:"ᾝ";s:3:"ᾞ";s:8:"ᾞ";s:3:"ᾟ";s:8:"ᾟ";s:3:"ᾠ";s:6:"ᾠ";s:3:"ᾡ";s:6:"ᾡ";s:3:"ᾢ";s:8:"ᾢ";s:3:"ᾣ";s:8:"ᾣ";s:3:"ᾤ";s:8:"ᾤ";s:3:"ᾥ";s:8:"ᾥ";s:3:"ᾦ";s:8:"ᾦ";s:3:"ᾧ";s:8:"ᾧ";s:3:"ᾨ";s:6:"ᾨ";s:3:"ᾩ";s:6:"ᾩ";s:3:"ᾪ";s:8:"ᾪ";s:3:"ᾫ";s:8:"ᾫ";s:3:"ᾬ";s:8:"ᾬ";s:3:"ᾭ";s:8:"ᾭ";s:3:"ᾮ";s:8:"ᾮ";s:3:"ᾯ";s:8:"ᾯ";s:3:"ᾰ";s:4:"ᾰ";s:3:"ᾱ";s:4:"ᾱ";s:3:"ᾲ";s:6:"ᾲ";s:3:"ᾳ";s:4:"ᾳ";s:3:"ᾴ";s:6:"ᾴ";s:3:"ᾶ";s:4:"ᾶ";s:3:"ᾷ";s:6:"ᾷ";s:3:"Ᾰ";s:4:"Ᾰ";s:3:"Ᾱ";s:4:"Ᾱ";s:3:"Ὰ";s:4:"Ὰ";s:3:"Ά";s:4:"Ά";s:3:"ᾼ";s:4:"ᾼ";s:3:"ι";s:2:"ι";s:3:"῁";s:4:"῁";s:3:"ῂ";s:6:"ῂ";s:3:"ῃ";s:4:"ῃ";s:3:"ῄ";s:6:"ῄ";s:3:"ῆ";s:4:"ῆ";s:3:"ῇ";s:6:"ῇ";s:3:"Ὲ";s:4:"Ὲ";s:3:"Έ";s:4:"Έ";s:3:"Ὴ";s:4:"Ὴ";s:3:"Ή";s:4:"Ή";s:3:"ῌ";s:4:"ῌ";s:3:"῍";s:5:"῍";s:3:"῎";s:5:"῎";s:3:"῏";s:5:"῏";s:3:"ῐ";s:4:"ῐ";s:3:"ῑ";s:4:"ῑ";s:3:"ῒ";s:6:"ῒ";s:3:"ΐ";s:6:"ΐ";s:3:"ῖ";s:4:"ῖ";s:3:"ῗ";s:6:"ῗ";s:3:"Ῐ";s:4:"Ῐ";s:3:"Ῑ";s:4:"Ῑ";s:3:"Ὶ";s:4:"Ὶ";s:3:"Ί";s:4:"Ί";s:3:"῝";s:5:"῝";s:3:"῞";s:5:"῞";s:3:"῟";s:5:"῟";s:3:"ῠ";s:4:"ῠ";s:3:"ῡ";s:4:"ῡ";s:3:"ῢ";s:6:"ῢ";s:3:"ΰ";s:6:"ΰ";s:3:"ῤ";s:4:"ῤ";s:3:"ῥ";s:4:"ῥ";s:3:"ῦ";s:4:"ῦ";s:3:"ῧ";s:6:"ῧ";s:3:"Ῠ";s:4:"Ῠ";s:3:"Ῡ";s:4:"Ῡ";s:3:"Ὺ";s:4:"Ὺ";s:3:"Ύ";s:4:"Ύ";s:3:"Ῥ";s:4:"Ῥ";s:3:"῭";s:4:"῭";s:3:"΅";s:4:"΅";s:3:"`";s:1:"`";s:3:"ῲ";s:6:"ῲ";s:3:"ῳ";s:4:"ῳ";s:3:"ῴ";s:6:"ῴ";s:3:"ῶ";s:4:"ῶ";s:3:"ῷ";s:6:"ῷ";s:3:"Ὸ";s:4:"Ὸ";s:3:"Ό";s:4:"Ό";s:3:"Ὼ";s:4:"Ὼ";s:3:"Ώ";s:4:"Ώ";s:3:"ῼ";s:4:"ῼ";s:3:"´";s:2:"´";s:3:" ";s:3:" ";s:3:" ";s:3:" ";s:3:"Ω";s:2:"Ω";s:3:"K";s:1:"K";s:3:"Å";s:3:"Å";s:3:"↚";s:5:"↚";s:3:"↛";s:5:"↛";s:3:"↮";s:5:"↮";s:3:"⇍";s:5:"⇍";s:3:"⇎";s:5:"⇎";s:3:"⇏";s:5:"⇏";s:3:"∄";s:5:"∄";s:3:"∉";s:5:"∉";s:3:"∌";s:5:"∌";s:3:"∤";s:5:"∤";s:3:"∦";s:5:"∦";s:3:"≁";s:5:"≁";s:3:"≄";s:5:"≄";s:3:"≇";s:5:"≇";s:3:"≉";s:5:"≉";s:3:"≠";s:3:"≠";s:3:"≢";s:5:"≢";s:3:"≭";s:5:"≭";s:3:"≮";s:3:"≮";s:3:"≯";s:3:"≯";s:3:"≰";s:5:"≰";s:3:"≱";s:5:"≱";s:3:"≴";s:5:"≴";s:3:"≵";s:5:"≵";s:3:"≸";s:5:"≸";s:3:"≹";s:5:"≹";s:3:"⊀";s:5:"⊀";s:3:"⊁";s:5:"⊁";s:3:"⊄";s:5:"⊄";s:3:"⊅";s:5:"⊅";s:3:"⊈";s:5:"⊈";s:3:"⊉";s:5:"⊉";s:3:"⊬";s:5:"⊬";s:3:"⊭";s:5:"⊭";s:3:"⊮";s:5:"⊮";s:3:"⊯";s:5:"⊯";s:3:"⋠";s:5:"⋠";s:3:"⋡";s:5:"⋡";s:3:"⋢";s:5:"⋢";s:3:"⋣";s:5:"⋣";s:3:"⋪";s:5:"⋪";s:3:"⋫";s:5:"⋫";s:3:"⋬";s:5:"⋬";s:3:"⋭";s:5:"⋭";s:3:"〈";s:3:"〈";s:3:"〉";s:3:"〉";s:3:"⫝̸";s:5:"⫝̸";s:3:"が";s:6:"が";s:3:"ぎ";s:6:"ぎ";s:3:"ぐ";s:6:"ぐ";s:3:"げ";s:6:"げ";s:3:"ご";s:6:"ご";s:3:"ざ";s:6:"ざ";s:3:"じ";s:6:"じ";s:3:"ず";s:6:"ず";s:3:"ぜ";s:6:"ぜ";s:3:"ぞ";s:6:"ぞ";s:3:"だ";s:6:"だ";s:3:"ぢ";s:6:"ぢ";s:3:"づ";s:6:"づ";s:3:"で";s:6:"で";s:3:"ど";s:6:"ど";s:3:"ば";s:6:"ば";s:3:"ぱ";s:6:"ぱ";s:3:"び";s:6:"び";s:3:"ぴ";s:6:"ぴ";s:3:"ぶ";s:6:"ぶ";s:3:"ぷ";s:6:"ぷ";s:3:"べ";s:6:"べ";s:3:"ぺ";s:6:"ぺ";s:3:"ぼ";s:6:"ぼ";s:3:"ぽ";s:6:"ぽ";s:3:"ゔ";s:6:"ゔ";s:3:"ゞ";s:6:"ゞ";s:3:"ガ";s:6:"ガ";s:3:"ギ";s:6:"ギ";s:3:"グ";s:6:"グ";s:3:"ゲ";s:6:"ゲ";s:3:"ゴ";s:6:"ゴ";s:3:"ザ";s:6:"ザ";s:3:"ジ";s:6:"ジ";s:3:"ズ";s:6:"ズ";s:3:"ゼ";s:6:"ゼ";s:3:"ゾ";s:6:"ゾ";s:3:"ダ";s:6:"ダ";s:3:"ヂ";s:6:"ヂ";s:3:"ヅ";s:6:"ヅ";s:3:"デ";s:6:"デ";s:3:"ド";s:6:"ド";s:3:"バ";s:6:"バ";s:3:"パ";s:6:"パ";s:3:"ビ";s:6:"ビ";s:3:"ピ";s:6:"ピ";s:3:"ブ";s:6:"ブ";s:3:"プ";s:6:"プ";s:3:"ベ";s:6:"ベ";s:3:"ペ";s:6:"ペ";s:3:"ボ";s:6:"ボ";s:3:"ポ";s:6:"ポ";s:3:"ヴ";s:6:"ヴ";s:3:"ヷ";s:6:"ヷ";s:3:"ヸ";s:6:"ヸ";s:3:"ヹ";s:6:"ヹ";s:3:"ヺ";s:6:"ヺ";s:3:"ヾ";s:6:"ヾ";s:3:"豈";s:3:"豈";s:3:"更";s:3:"更";s:3:"車";s:3:"車";s:3:"賈";s:3:"賈";s:3:"滑";s:3:"滑";s:3:"串";s:3:"串";s:3:"句";s:3:"句";s:3:"龜";s:3:"龜";s:3:"龜";s:3:"龜";s:3:"契";s:3:"契";s:3:"金";s:3:"金";s:3:"喇";s:3:"喇";s:3:"奈";s:3:"奈";s:3:"懶";s:3:"懶";s:3:"癩";s:3:"癩";s:3:"羅";s:3:"羅";s:3:"蘿";s:3:"蘿";s:3:"螺";s:3:"螺";s:3:"裸";s:3:"裸";s:3:"邏";s:3:"邏";s:3:"樂";s:3:"樂";s:3:"洛";s:3:"洛";s:3:"烙";s:3:"烙";s:3:"珞";s:3:"珞";s:3:"落";s:3:"落";s:3:"酪";s:3:"酪";s:3:"駱";s:3:"駱";s:3:"亂";s:3:"亂";s:3:"卵";s:3:"卵";s:3:"欄";s:3:"欄";s:3:"爛";s:3:"爛";s:3:"蘭";s:3:"蘭";s:3:"鸞";s:3:"鸞";s:3:"嵐";s:3:"嵐";s:3:"濫";s:3:"濫";s:3:"藍";s:3:"藍";s:3:"襤";s:3:"襤";s:3:"拉";s:3:"拉";s:3:"臘";s:3:"臘";s:3:"蠟";s:3:"蠟";s:3:"廊";s:3:"廊";s:3:"朗";s:3:"朗";s:3:"浪";s:3:"浪";s:3:"狼";s:3:"狼";s:3:"郎";s:3:"郎";s:3:"來";s:3:"來";s:3:"冷";s:3:"冷";s:3:"勞";s:3:"勞";s:3:"擄";s:3:"擄";s:3:"櫓";s:3:"櫓";s:3:"爐";s:3:"爐";s:3:"盧";s:3:"盧";s:3:"老";s:3:"老";s:3:"蘆";s:3:"蘆";s:3:"虜";s:3:"虜";s:3:"路";s:3:"路";s:3:"露";s:3:"露";s:3:"魯";s:3:"魯";s:3:"鷺";s:3:"鷺";s:3:"碌";s:3:"碌";s:3:"祿";s:3:"祿";s:3:"綠";s:3:"綠";s:3:"菉";s:3:"菉";s:3:"錄";s:3:"錄";s:3:"鹿";s:3:"鹿";s:3:"論";s:3:"論";s:3:"壟";s:3:"壟";s:3:"弄";s:3:"弄";s:3:"籠";s:3:"籠";s:3:"聾";s:3:"聾";s:3:"牢";s:3:"牢";s:3:"磊";s:3:"磊";s:3:"賂";s:3:"賂";s:3:"雷";s:3:"雷";s:3:"壘";s:3:"壘";s:3:"屢";s:3:"屢";s:3:"樓";s:3:"樓";s:3:"淚";s:3:"淚";s:3:"漏";s:3:"漏";s:3:"累";s:3:"累";s:3:"縷";s:3:"縷";s:3:"陋";s:3:"陋";s:3:"勒";s:3:"勒";s:3:"肋";s:3:"肋";s:3:"凜";s:3:"凜";s:3:"凌";s:3:"凌";s:3:"稜";s:3:"稜";s:3:"綾";s:3:"綾";s:3:"菱";s:3:"菱";s:3:"陵";s:3:"陵";s:3:"讀";s:3:"讀";s:3:"拏";s:3:"拏";s:3:"樂";s:3:"樂";s:3:"諾";s:3:"諾";s:3:"丹";s:3:"丹";s:3:"寧";s:3:"寧";s:3:"怒";s:3:"怒";s:3:"率";s:3:"率";s:3:"異";s:3:"異";s:3:"北";s:3:"北";s:3:"磻";s:3:"磻";s:3:"便";s:3:"便";s:3:"復";s:3:"復";s:3:"不";s:3:"不";s:3:"泌";s:3:"泌";s:3:"數";s:3:"數";s:3:"索";s:3:"索";s:3:"參";s:3:"參";s:3:"塞";s:3:"塞";s:3:"省";s:3:"省";s:3:"葉";s:3:"葉";s:3:"說";s:3:"說";s:3:"殺";s:3:"殺";s:3:"辰";s:3:"辰";s:3:"沈";s:3:"沈";s:3:"拾";s:3:"拾";s:3:"若";s:3:"若";s:3:"掠";s:3:"掠";s:3:"略";s:3:"略";s:3:"亮";s:3:"亮";s:3:"兩";s:3:"兩";s:3:"凉";s:3:"凉";s:3:"梁";s:3:"梁";s:3:"糧";s:3:"糧";s:3:"良";s:3:"良";s:3:"諒";s:3:"諒";s:3:"量";s:3:"量";s:3:"勵";s:3:"勵";s:3:"呂";s:3:"呂";s:3:"女";s:3:"女";s:3:"廬";s:3:"廬";s:3:"旅";s:3:"旅";s:3:"濾";s:3:"濾";s:3:"礪";s:3:"礪";s:3:"閭";s:3:"閭";s:3:"驪";s:3:"驪";s:3:"麗";s:3:"麗";s:3:"黎";s:3:"黎";s:3:"力";s:3:"力";s:3:"曆";s:3:"曆";s:3:"歷";s:3:"歷";s:3:"轢";s:3:"轢";s:3:"年";s:3:"年";s:3:"憐";s:3:"憐";s:3:"戀";s:3:"戀";s:3:"撚";s:3:"撚";s:3:"漣";s:3:"漣";s:3:"煉";s:3:"煉";s:3:"璉";s:3:"璉";s:3:"秊";s:3:"秊";s:3:"練";s:3:"練";s:3:"聯";s:3:"聯";s:3:"輦";s:3:"輦";s:3:"蓮";s:3:"蓮";s:3:"連";s:3:"連";s:3:"鍊";s:3:"鍊";s:3:"列";s:3:"列";s:3:"劣";s:3:"劣";s:3:"咽";s:3:"咽";s:3:"烈";s:3:"烈";s:3:"裂";s:3:"裂";s:3:"說";s:3:"說";s:3:"廉";s:3:"廉";s:3:"念";s:3:"念";s:3:"捻";s:3:"捻";s:3:"殮";s:3:"殮";s:3:"簾";s:3:"簾";s:3:"獵";s:3:"獵";s:3:"令";s:3:"令";s:3:"囹";s:3:"囹";s:3:"寧";s:3:"寧";s:3:"嶺";s:3:"嶺";s:3:"怜";s:3:"怜";s:3:"玲";s:3:"玲";s:3:"瑩";s:3:"瑩";s:3:"羚";s:3:"羚";s:3:"聆";s:3:"聆";s:3:"鈴";s:3:"鈴";s:3:"零";s:3:"零";s:3:"靈";s:3:"靈";s:3:"領";s:3:"領";s:3:"例";s:3:"例";s:3:"禮";s:3:"禮";s:3:"醴";s:3:"醴";s:3:"隸";s:3:"隸";s:3:"惡";s:3:"惡";s:3:"了";s:3:"了";s:3:"僚";s:3:"僚";s:3:"寮";s:3:"寮";s:3:"尿";s:3:"尿";s:3:"料";s:3:"料";s:3:"樂";s:3:"樂";s:3:"燎";s:3:"燎";s:3:"療";s:3:"療";s:3:"蓼";s:3:"蓼";s:3:"遼";s:3:"遼";s:3:"龍";s:3:"龍";s:3:"暈";s:3:"暈";s:3:"阮";s:3:"阮";s:3:"劉";s:3:"劉";s:3:"杻";s:3:"杻";s:3:"柳";s:3:"柳";s:3:"流";s:3:"流";s:3:"溜";s:3:"溜";s:3:"琉";s:3:"琉";s:3:"留";s:3:"留";s:3:"硫";s:3:"硫";s:3:"紐";s:3:"紐";s:3:"類";s:3:"類";s:3:"六";s:3:"六";s:3:"戮";s:3:"戮";s:3:"陸";s:3:"陸";s:3:"倫";s:3:"倫";s:3:"崙";s:3:"崙";s:3:"淪";s:3:"淪";s:3:"輪";s:3:"輪";s:3:"律";s:3:"律";s:3:"慄";s:3:"慄";s:3:"栗";s:3:"栗";s:3:"率";s:3:"率";s:3:"隆";s:3:"隆";s:3:"利";s:3:"利";s:3:"吏";s:3:"吏";s:3:"履";s:3:"履";s:3:"易";s:3:"易";s:3:"李";s:3:"李";s:3:"梨";s:3:"梨";s:3:"泥";s:3:"泥";s:3:"理";s:3:"理";s:3:"痢";s:3:"痢";s:3:"罹";s:3:"罹";s:3:"裏";s:3:"裏";s:3:"裡";s:3:"裡";s:3:"里";s:3:"里";s:3:"離";s:3:"離";s:3:"匿";s:3:"匿";s:3:"溺";s:3:"溺";s:3:"吝";s:3:"吝";s:3:"燐";s:3:"燐";s:3:"璘";s:3:"璘";s:3:"藺";s:3:"藺";s:3:"隣";s:3:"隣";s:3:"鱗";s:3:"鱗";s:3:"麟";s:3:"麟";s:3:"林";s:3:"林";s:3:"淋";s:3:"淋";s:3:"臨";s:3:"臨";s:3:"立";s:3:"立";s:3:"笠";s:3:"笠";s:3:"粒";s:3:"粒";s:3:"狀";s:3:"狀";s:3:"炙";s:3:"炙";s:3:"識";s:3:"識";s:3:"什";s:3:"什";s:3:"茶";s:3:"茶";s:3:"刺";s:3:"刺";s:3:"切";s:3:"切";s:3:"度";s:3:"度";s:3:"拓";s:3:"拓";s:3:"糖";s:3:"糖";s:3:"宅";s:3:"宅";s:3:"洞";s:3:"洞";s:3:"暴";s:3:"暴";s:3:"輻";s:3:"輻";s:3:"行";s:3:"行";s:3:"降";s:3:"降";s:3:"見";s:3:"見";s:3:"廓";s:3:"廓";s:3:"兀";s:3:"兀";s:3:"嗀";s:3:"嗀";s:3:"塚";s:3:"塚";s:3:"晴";s:3:"晴";s:3:"凞";s:3:"凞";s:3:"猪";s:3:"猪";s:3:"益";s:3:"益";s:3:"礼";s:3:"礼";s:3:"神";s:3:"神";s:3:"祥";s:3:"祥";s:3:"福";s:3:"福";s:3:"靖";s:3:"靖";s:3:"精";s:3:"精";s:3:"羽";s:3:"羽";s:3:"蘒";s:3:"蘒";s:3:"諸";s:3:"諸";s:3:"逸";s:3:"逸";s:3:"都";s:3:"都";s:3:"飯";s:3:"飯";s:3:"飼";s:3:"飼";s:3:"館";s:3:"館";s:3:"鶴";s:3:"鶴";s:3:"侮";s:3:"侮";s:3:"僧";s:3:"僧";s:3:"免";s:3:"免";s:3:"勉";s:3:"勉";s:3:"勤";s:3:"勤";s:3:"卑";s:3:"卑";s:3:"喝";s:3:"喝";s:3:"嘆";s:3:"嘆";s:3:"器";s:3:"器";s:3:"塀";s:3:"塀";s:3:"墨";s:3:"墨";s:3:"層";s:3:"層";s:3:"屮";s:3:"屮";s:3:"悔";s:3:"悔";s:3:"慨";s:3:"慨";s:3:"憎";s:3:"憎";s:3:"懲";s:3:"懲";s:3:"敏";s:3:"敏";s:3:"既";s:3:"既";s:3:"暑";s:3:"暑";s:3:"梅";s:3:"梅";s:3:"海";s:3:"海";s:3:"渚";s:3:"渚";s:3:"漢";s:3:"漢";s:3:"煮";s:3:"煮";s:3:"爫";s:3:"爫";s:3:"琢";s:3:"琢";s:3:"碑";s:3:"碑";s:3:"社";s:3:"社";s:3:"祉";s:3:"祉";s:3:"祈";s:3:"祈";s:3:"祐";s:3:"祐";s:3:"祖";s:3:"祖";s:3:"祝";s:3:"祝";s:3:"禍";s:3:"禍";s:3:"禎";s:3:"禎";s:3:"穀";s:3:"穀";s:3:"突";s:3:"突";s:3:"節";s:3:"節";s:3:"練";s:3:"練";s:3:"縉";s:3:"縉";s:3:"繁";s:3:"繁";s:3:"署";s:3:"署";s:3:"者";s:3:"者";s:3:"臭";s:3:"臭";s:3:"艹";s:3:"艹";s:3:"艹";s:3:"艹";s:3:"著";s:3:"著";s:3:"褐";s:3:"褐";s:3:"視";s:3:"視";s:3:"謁";s:3:"謁";s:3:"謹";s:3:"謹";s:3:"賓";s:3:"賓";s:3:"贈";s:3:"贈";s:3:"辶";s:3:"辶";s:3:"逸";s:3:"逸";s:3:"難";s:3:"難";s:3:"響";s:3:"響";s:3:"頻";s:3:"頻";s:3:"並";s:3:"並";s:3:"况";s:3:"况";s:3:"全";s:3:"全";s:3:"侀";s:3:"侀";s:3:"充";s:3:"充";s:3:"冀";s:3:"冀";s:3:"勇";s:3:"勇";s:3:"勺";s:3:"勺";s:3:"喝";s:3:"喝";s:3:"啕";s:3:"啕";s:3:"喙";s:3:"喙";s:3:"嗢";s:3:"嗢";s:3:"塚";s:3:"塚";s:3:"墳";s:3:"墳";s:3:"奄";s:3:"奄";s:3:"奔";s:3:"奔";s:3:"婢";s:3:"婢";s:3:"嬨";s:3:"嬨";s:3:"廒";s:3:"廒";s:3:"廙";s:3:"廙";s:3:"彩";s:3:"彩";s:3:"徭";s:3:"徭";s:3:"惘";s:3:"惘";s:3:"慎";s:3:"慎";s:3:"愈";s:3:"愈";s:3:"憎";s:3:"憎";s:3:"慠";s:3:"慠";s:3:"懲";s:3:"懲";s:3:"戴";s:3:"戴";s:3:"揄";s:3:"揄";s:3:"搜";s:3:"搜";s:3:"摒";s:3:"摒";s:3:"敖";s:3:"敖";s:3:"晴";s:3:"晴";s:3:"朗";s:3:"朗";s:3:"望";s:3:"望";s:3:"杖";s:3:"杖";s:3:"歹";s:3:"歹";s:3:"殺";s:3:"殺";s:3:"流";s:3:"流";s:3:"滛";s:3:"滛";s:3:"滋";s:3:"滋";s:3:"漢";s:3:"漢";s:3:"瀞";s:3:"瀞";s:3:"煮";s:3:"煮";s:3:"瞧";s:3:"瞧";s:3:"爵";s:3:"爵";s:3:"犯";s:3:"犯";s:3:"猪";s:3:"猪";s:3:"瑱";s:3:"瑱";s:3:"甆";s:3:"甆";s:3:"画";s:3:"画";s:3:"瘝";s:3:"瘝";s:3:"瘟";s:3:"瘟";s:3:"益";s:3:"益";s:3:"盛";s:3:"盛";s:3:"直";s:3:"直";s:3:"睊";s:3:"睊";s:3:"着";s:3:"着";s:3:"磌";s:3:"磌";s:3:"窱";s:3:"窱";s:3:"節";s:3:"節";s:3:"类";s:3:"类";s:3:"絛";s:3:"絛";s:3:"練";s:3:"練";s:3:"缾";s:3:"缾";s:3:"者";s:3:"者";s:3:"荒";s:3:"荒";s:3:"華";s:3:"華";s:3:"蝹";s:3:"蝹";s:3:"襁";s:3:"襁";s:3:"覆";s:3:"覆";s:3:"視";s:3:"視";s:3:"調";s:3:"調";s:3:"諸";s:3:"諸";s:3:"請";s:3:"請";s:3:"謁";s:3:"謁";s:3:"諾";s:3:"諾";s:3:"諭";s:3:"諭";s:3:"謹";s:3:"謹";s:3:"變";s:3:"變";s:3:"贈";s:3:"贈";s:3:"輸";s:3:"輸";s:3:"遲";s:3:"遲";s:3:"醙";s:3:"醙";s:3:"鉶";s:3:"鉶";s:3:"陼";s:3:"陼";s:3:"難";s:3:"難";s:3:"靖";s:3:"靖";s:3:"韛";s:3:"韛";s:3:"響";s:3:"響";s:3:"頋";s:3:"頋";s:3:"頻";s:3:"頻";s:3:"鬒";s:3:"鬒";s:3:"龜";s:3:"龜";s:3:"𢡊";s:4:"𢡊";s:3:"𢡄";s:4:"𢡄";s:3:"𣏕";s:4:"𣏕";s:3:"㮝";s:3:"㮝";s:3:"䀘";s:3:"䀘";s:3:"䀹";s:3:"䀹";s:3:"𥉉";s:4:"𥉉";s:3:"𥳐";s:4:"𥳐";s:3:"𧻓";s:4:"𧻓";s:3:"齃";s:3:"齃";s:3:"龎";s:3:"龎";s:3:"יִ";s:4:"יִ";s:3:"ײַ";s:4:"ײַ";s:3:"שׁ";s:4:"שׁ";s:3:"שׂ";s:4:"שׂ";s:3:"שּׁ";s:6:"שּׁ";s:3:"שּׂ";s:6:"שּׂ";s:3:"אַ";s:4:"אַ";s:3:"אָ";s:4:"אָ";s:3:"אּ";s:4:"אּ";s:3:"בּ";s:4:"בּ";s:3:"גּ";s:4:"גּ";s:3:"דּ";s:4:"דּ";s:3:"הּ";s:4:"הּ";s:3:"וּ";s:4:"וּ";s:3:"זּ";s:4:"זּ";s:3:"טּ";s:4:"טּ";s:3:"יּ";s:4:"יּ";s:3:"ךּ";s:4:"ךּ";s:3:"כּ";s:4:"כּ";s:3:"לּ";s:4:"לּ";s:3:"מּ";s:4:"מּ";s:3:"נּ";s:4:"נּ";s:3:"סּ";s:4:"סּ";s:3:"ףּ";s:4:"ףּ";s:3:"פּ";s:4:"פּ";s:3:"צּ";s:4:"צּ";s:3:"קּ";s:4:"קּ";s:3:"רּ";s:4:"רּ";s:3:"שּ";s:4:"שּ";s:3:"תּ";s:4:"תּ";s:3:"וֹ";s:4:"וֹ";s:3:"בֿ";s:4:"בֿ";s:3:"כֿ";s:4:"כֿ";s:3:"פֿ";s:4:"פֿ";s:4:"𝅗𝅥";s:8:"𝅗𝅥";s:4:"𝅘𝅥";s:8:"𝅘𝅥";s:4:"𝅘𝅥𝅮";s:12:"𝅘𝅥𝅮";s:4:"𝅘𝅥𝅯";s:12:"𝅘𝅥𝅯";s:4:"𝅘𝅥𝅰";s:12:"𝅘𝅥𝅰";s:4:"𝅘𝅥𝅱";s:12:"𝅘𝅥𝅱";s:4:"𝅘𝅥𝅲";s:12:"𝅘𝅥𝅲";s:4:"𝆹𝅥";s:8:"𝆹𝅥";s:4:"𝆺𝅥";s:8:"𝆺𝅥";s:4:"𝆹𝅥𝅮";s:12:"𝆹𝅥𝅮";s:4:"𝆺𝅥𝅮";s:12:"𝆺𝅥𝅮";s:4:"𝆹𝅥𝅯";s:12:"𝆹𝅥𝅯";s:4:"𝆺𝅥𝅯";s:12:"𝆺𝅥𝅯";s:4:"丽";s:3:"丽";s:4:"丸";s:3:"丸";s:4:"乁";s:3:"乁";s:4:"𠄢";s:4:"𠄢";s:4:"你";s:3:"你";s:4:"侮";s:3:"侮";s:4:"侻";s:3:"侻";s:4:"倂";s:3:"倂";s:4:"偺";s:3:"偺";s:4:"備";s:3:"備";s:4:"僧";s:3:"僧";s:4:"像";s:3:"像";s:4:"㒞";s:3:"㒞";s:4:"𠘺";s:4:"𠘺";s:4:"免";s:3:"免";s:4:"兔";s:3:"兔";s:4:"兤";s:3:"兤";s:4:"具";s:3:"具";s:4:"𠔜";s:4:"𠔜";s:4:"㒹";s:3:"㒹";s:4:"內";s:3:"內";s:4:"再";s:3:"再";s:4:"𠕋";s:4:"𠕋";s:4:"冗";s:3:"冗";s:4:"冤";s:3:"冤";s:4:"仌";s:3:"仌";s:4:"冬";s:3:"冬";s:4:"况";s:3:"况";s:4:"𩇟";s:4:"𩇟";s:4:"凵";s:3:"凵";s:4:"刃";s:3:"刃";s:4:"㓟";s:3:"㓟";s:4:"刻";s:3:"刻";s:4:"剆";s:3:"剆";s:4:"割";s:3:"割";s:4:"剷";s:3:"剷";s:4:"㔕";s:3:"㔕";s:4:"勇";s:3:"勇";s:4:"勉";s:3:"勉";s:4:"勤";s:3:"勤";s:4:"勺";s:3:"勺";s:4:"包";s:3:"包";s:4:"匆";s:3:"匆";s:4:"北";s:3:"北";s:4:"卉";s:3:"卉";s:4:"卑";s:3:"卑";s:4:"博";s:3:"博";s:4:"即";s:3:"即";s:4:"卽";s:3:"卽";s:4:"卿";s:3:"卿";s:4:"卿";s:3:"卿";s:4:"卿";s:3:"卿";s:4:"𠨬";s:4:"𠨬";s:4:"灰";s:3:"灰";s:4:"及";s:3:"及";s:4:"叟";s:3:"叟";s:4:"𠭣";s:4:"𠭣";s:4:"叫";s:3:"叫";s:4:"叱";s:3:"叱";s:4:"吆";s:3:"吆";s:4:"咞";s:3:"咞";s:4:"吸";s:3:"吸";s:4:"呈";s:3:"呈";s:4:"周";s:3:"周";s:4:"咢";s:3:"咢";s:4:"哶";s:3:"哶";s:4:"唐";s:3:"唐";s:4:"啓";s:3:"啓";s:4:"啣";s:3:"啣";s:4:"善";s:3:"善";s:4:"善";s:3:"善";s:4:"喙";s:3:"喙";s:4:"喫";s:3:"喫";s:4:"喳";s:3:"喳";s:4:"嗂";s:3:"嗂";s:4:"圖";s:3:"圖";s:4:"嘆";s:3:"嘆";s:4:"圗";s:3:"圗";s:4:"噑";s:3:"噑";s:4:"噴";s:3:"噴";s:4:"切";s:3:"切";s:4:"壮";s:3:"壮";s:4:"城";s:3:"城";s:4:"埴";s:3:"埴";s:4:"堍";s:3:"堍";s:4:"型";s:3:"型";s:4:"堲";s:3:"堲";s:4:"報";s:3:"報";s:4:"墬";s:3:"墬";s:4:"𡓤";s:4:"𡓤";s:4:"売";s:3:"売";s:4:"壷";s:3:"壷";s:4:"夆";s:3:"夆";s:4:"多";s:3:"多";s:4:"夢";s:3:"夢";s:4:"奢";s:3:"奢";s:4:"𡚨";s:4:"𡚨";s:4:"𡛪";s:4:"𡛪";s:4:"姬";s:3:"姬";s:4:"娛";s:3:"娛";s:4:"娧";s:3:"娧";s:4:"姘";s:3:"姘";s:4:"婦";s:3:"婦";s:4:"㛮";s:3:"㛮";s:4:"㛼";s:3:"㛼";s:4:"嬈";s:3:"嬈";s:4:"嬾";s:3:"嬾";s:4:"嬾";s:3:"嬾";s:4:"𡧈";s:4:"𡧈";s:4:"寃";s:3:"寃";s:4:"寘";s:3:"寘";s:4:"寧";s:3:"寧";s:4:"寳";s:3:"寳";s:4:"𡬘";s:4:"𡬘";s:4:"寿";s:3:"寿";s:4:"将";s:3:"将";s:4:"当";s:3:"当";s:4:"尢";s:3:"尢";s:4:"㞁";s:3:"㞁";s:4:"屠";s:3:"屠";s:4:"屮";s:3:"屮";s:4:"峀";s:3:"峀";s:4:"岍";s:3:"岍";s:4:"𡷤";s:4:"𡷤";s:4:"嵃";s:3:"嵃";s:4:"𡷦";s:4:"𡷦";s:4:"嵮";s:3:"嵮";s:4:"嵫";s:3:"嵫";s:4:"嵼";s:3:"嵼";s:4:"巡";s:3:"巡";s:4:"巢";s:3:"巢";s:4:"㠯";s:3:"㠯";s:4:"巽";s:3:"巽";s:4:"帨";s:3:"帨";s:4:"帽";s:3:"帽";s:4:"幩";s:3:"幩";s:4:"㡢";s:3:"㡢";s:4:"𢆃";s:4:"𢆃";s:4:"㡼";s:3:"㡼";s:4:"庰";s:3:"庰";s:4:"庳";s:3:"庳";s:4:"庶";s:3:"庶";s:4:"廊";s:3:"廊";s:4:"𪎒";s:4:"𪎒";s:4:"廾";s:3:"廾";s:4:"𢌱";s:4:"𢌱";s:4:"𢌱";s:4:"𢌱";s:4:"舁";s:3:"舁";s:4:"弢";s:3:"弢";s:4:"弢";s:3:"弢";s:4:"㣇";s:3:"㣇";s:4:"𣊸";s:4:"𣊸";s:4:"𦇚";s:4:"𦇚";s:4:"形";s:3:"形";s:4:"彫";s:3:"彫";s:4:"㣣";s:3:"㣣";s:4:"徚";s:3:"徚";s:4:"忍";s:3:"忍";s:4:"志";s:3:"志";s:4:"忹";s:3:"忹";s:4:"悁";s:3:"悁";s:4:"㤺";s:3:"㤺";s:4:"㤜";s:3:"㤜";s:4:"悔";s:3:"悔";s:4:"𢛔";s:4:"𢛔";s:4:"惇";s:3:"惇";s:4:"慈";s:3:"慈";s:4:"慌";s:3:"慌";s:4:"慎";s:3:"慎";s:4:"慌";s:3:"慌";s:4:"慺";s:3:"慺";s:4:"憎";s:3:"憎";s:4:"憲";s:3:"憲";s:4:"憤";s:3:"憤";s:4:"憯";s:3:"憯";s:4:"懞";s:3:"懞";s:4:"懲";s:3:"懲";s:4:"懶";s:3:"懶";s:4:"成";s:3:"成";s:4:"戛";s:3:"戛";s:4:"扝";s:3:"扝";s:4:"抱";s:3:"抱";s:4:"拔";s:3:"拔";s:4:"捐";s:3:"捐";s:4:"𢬌";s:4:"𢬌";s:4:"挽";s:3:"挽";s:4:"拼";s:3:"拼";s:4:"捨";s:3:"捨";s:4:"掃";s:3:"掃";s:4:"揤";s:3:"揤";s:4:"𢯱";s:4:"𢯱";s:4:"搢";s:3:"搢";s:4:"揅";s:3:"揅";s:4:"掩";s:3:"掩";s:4:"㨮";s:3:"㨮";s:4:"摩";s:3:"摩";s:4:"摾";s:3:"摾";s:4:"撝";s:3:"撝";s:4:"摷";s:3:"摷";s:4:"㩬";s:3:"㩬";s:4:"敏";s:3:"敏";s:4:"敬";s:3:"敬";s:4:"𣀊";s:4:"𣀊";s:4:"旣";s:3:"旣";s:4:"書";s:3:"書";s:4:"晉";s:3:"晉";s:4:"㬙";s:3:"㬙";s:4:"暑";s:3:"暑";s:4:"㬈";s:3:"㬈";s:4:"㫤";s:3:"㫤";s:4:"冒";s:3:"冒";s:4:"冕";s:3:"冕";s:4:"最";s:3:"最";s:4:"暜";s:3:"暜";s:4:"肭";s:3:"肭";s:4:"䏙";s:3:"䏙";s:4:"朗";s:3:"朗";s:4:"望";s:3:"望";s:4:"朡";s:3:"朡";s:4:"杞";s:3:"杞";s:4:"杓";s:3:"杓";s:4:"𣏃";s:4:"𣏃";s:4:"㭉";s:3:"㭉";s:4:"柺";s:3:"柺";s:4:"枅";s:3:"枅";s:4:"桒";s:3:"桒";s:4:"梅";s:3:"梅";s:4:"𣑭";s:4:"𣑭";s:4:"梎";s:3:"梎";s:4:"栟";s:3:"栟";s:4:"椔";s:3:"椔";s:4:"㮝";s:3:"㮝";s:4:"楂";s:3:"楂";s:4:"榣";s:3:"榣";s:4:"槪";s:3:"槪";s:4:"檨";s:3:"檨";s:4:"𣚣";s:4:"𣚣";s:4:"櫛";s:3:"櫛";s:4:"㰘";s:3:"㰘";s:4:"次";s:3:"次";s:4:"𣢧";s:4:"𣢧";s:4:"歔";s:3:"歔";s:4:"㱎";s:3:"㱎";s:4:"歲";s:3:"歲";s:4:"殟";s:3:"殟";s:4:"殺";s:3:"殺";s:4:"殻";s:3:"殻";s:4:"𣪍";s:4:"𣪍";s:4:"𡴋";s:4:"𡴋";s:4:"𣫺";s:4:"𣫺";s:4:"汎";s:3:"汎";s:4:"𣲼";s:4:"𣲼";s:4:"沿";s:3:"沿";s:4:"泍";s:3:"泍";s:4:"汧";s:3:"汧";s:4:"洖";s:3:"洖";s:4:"派";s:3:"派";s:4:"海";s:3:"海";s:4:"流";s:3:"流";s:4:"浩";s:3:"浩";s:4:"浸";s:3:"浸";s:4:"涅";s:3:"涅";s:4:"𣴞";s:4:"𣴞";s:4:"洴";s:3:"洴";s:4:"港";s:3:"港";s:4:"湮";s:3:"湮";s:4:"㴳";s:3:"㴳";s:4:"滋";s:3:"滋";s:4:"滇";s:3:"滇";s:4:"𣻑";s:4:"𣻑";s:4:"淹";s:3:"淹";s:4:"潮";s:3:"潮";s:4:"𣽞";s:4:"𣽞";s:4:"𣾎";s:4:"𣾎";s:4:"濆";s:3:"濆";s:4:"瀹";s:3:"瀹";s:4:"瀞";s:3:"瀞";s:4:"瀛";s:3:"瀛";s:4:"㶖";s:3:"㶖";s:4:"灊";s:3:"灊";s:4:"災";s:3:"災";s:4:"灷";s:3:"灷";s:4:"炭";s:3:"炭";s:4:"𠔥";s:4:"𠔥";s:4:"煅";s:3:"煅";s:4:"𤉣";s:4:"𤉣";s:4:"熜";s:3:"熜";s:4:"𤎫";s:4:"𤎫";s:4:"爨";s:3:"爨";s:4:"爵";s:3:"爵";s:4:"牐";s:3:"牐";s:4:"𤘈";s:4:"𤘈";s:4:"犀";s:3:"犀";s:4:"犕";s:3:"犕";s:4:"𤜵";s:4:"𤜵";s:4:"𤠔";s:4:"𤠔";s:4:"獺";s:3:"獺";s:4:"王";s:3:"王";s:4:"㺬";s:3:"㺬";s:4:"玥";s:3:"玥";s:4:"㺸";s:3:"㺸";s:4:"㺸";s:3:"㺸";s:4:"瑇";s:3:"瑇";s:4:"瑜";s:3:"瑜";s:4:"瑱";s:3:"瑱";s:4:"璅";s:3:"璅";s:4:"瓊";s:3:"瓊";s:4:"㼛";s:3:"㼛";s:4:"甤";s:3:"甤";s:4:"𤰶";s:4:"𤰶";s:4:"甾";s:3:"甾";s:4:"𤲒";s:4:"𤲒";s:4:"異";s:3:"異";s:4:"𢆟";s:4:"𢆟";s:4:"瘐";s:3:"瘐";s:4:"𤾡";s:4:"𤾡";s:4:"𤾸";s:4:"𤾸";s:4:"𥁄";s:4:"𥁄";s:4:"㿼";s:3:"㿼";s:4:"䀈";s:3:"䀈";s:4:"直";s:3:"直";s:4:"𥃳";s:4:"𥃳";s:4:"𥃲";s:4:"𥃲";s:4:"𥄙";s:4:"𥄙";s:4:"𥄳";s:4:"𥄳";s:4:"眞";s:3:"眞";s:4:"真";s:3:"真";s:4:"真";s:3:"真";s:4:"睊";s:3:"睊";s:4:"䀹";s:3:"䀹";s:4:"瞋";s:3:"瞋";s:4:"䁆";s:3:"䁆";s:4:"䂖";s:3:"䂖";s:4:"𥐝";s:4:"𥐝";s:4:"硎";s:3:"硎";s:4:"碌";s:3:"碌";s:4:"磌";s:3:"磌";s:4:"䃣";s:3:"䃣";s:4:"𥘦";s:4:"𥘦";s:4:"祖";s:3:"祖";s:4:"𥚚";s:4:"𥚚";s:4:"𥛅";s:4:"𥛅";s:4:"福";s:3:"福";s:4:"秫";s:3:"秫";s:4:"䄯";s:3:"䄯";s:4:"穀";s:3:"穀";s:4:"穊";s:3:"穊";s:4:"穏";s:3:"穏";s:4:"𥥼";s:4:"𥥼";s:4:"𥪧";s:4:"𥪧";s:4:"𥪧";s:4:"𥪧";s:4:"竮";s:3:"竮";s:4:"䈂";s:3:"䈂";s:4:"𥮫";s:4:"𥮫";s:4:"篆";s:3:"篆";s:4:"築";s:3:"築";s:4:"䈧";s:3:"䈧";s:4:"𥲀";s:4:"𥲀";s:4:"糒";s:3:"糒";s:4:"䊠";s:3:"䊠";s:4:"糨";s:3:"糨";s:4:"糣";s:3:"糣";s:4:"紀";s:3:"紀";s:4:"𥾆";s:4:"𥾆";s:4:"絣";s:3:"絣";s:4:"䌁";s:3:"䌁";s:4:"緇";s:3:"緇";s:4:"縂";s:3:"縂";s:4:"繅";s:3:"繅";s:4:"䌴";s:3:"䌴";s:4:"𦈨";s:4:"𦈨";s:4:"𦉇";s:4:"𦉇";s:4:"䍙";s:3:"䍙";s:4:"𦋙";s:4:"𦋙";s:4:"罺";s:3:"罺";s:4:"𦌾";s:4:"𦌾";s:4:"羕";s:3:"羕";s:4:"翺";s:3:"翺";s:4:"者";s:3:"者";s:4:"𦓚";s:4:"𦓚";s:4:"𦔣";s:4:"𦔣";s:4:"聠";s:3:"聠";s:4:"𦖨";s:4:"𦖨";s:4:"聰";s:3:"聰";s:4:"𣍟";s:4:"𣍟";s:4:"䏕";s:3:"䏕";s:4:"育";s:3:"育";s:4:"脃";s:3:"脃";s:4:"䐋";s:3:"䐋";s:4:"脾";s:3:"脾";s:4:"媵";s:3:"媵";s:4:"𦞧";s:4:"𦞧";s:4:"𦞵";s:4:"𦞵";s:4:"𣎓";s:4:"𣎓";s:4:"𣎜";s:4:"𣎜";s:4:"舁";s:3:"舁";s:4:"舄";s:3:"舄";s:4:"辞";s:3:"辞";s:4:"䑫";s:3:"䑫";s:4:"芑";s:3:"芑";s:4:"芋";s:3:"芋";s:4:"芝";s:3:"芝";s:4:"劳";s:3:"劳";s:4:"花";s:3:"花";s:4:"芳";s:3:"芳";s:4:"芽";s:3:"芽";s:4:"苦";s:3:"苦";s:4:"𦬼";s:4:"𦬼";s:4:"若";s:3:"若";s:4:"茝";s:3:"茝";s:4:"荣";s:3:"荣";s:4:"莭";s:3:"莭";s:4:"茣";s:3:"茣";s:4:"莽";s:3:"莽";s:4:"菧";s:3:"菧";s:4:"著";s:3:"著";s:4:"荓";s:3:"荓";s:4:"菊";s:3:"菊";s:4:"菌";s:3:"菌";s:4:"菜";s:3:"菜";s:4:"𦰶";s:4:"𦰶";s:4:"𦵫";s:4:"𦵫";s:4:"𦳕";s:4:"𦳕";s:4:"䔫";s:3:"䔫";s:4:"蓱";s:3:"蓱";s:4:"蓳";s:3:"蓳";s:4:"蔖";s:3:"蔖";s:4:"𧏊";s:4:"𧏊";s:4:"蕤";s:3:"蕤";s:4:"𦼬";s:4:"𦼬";s:4:"䕝";s:3:"䕝";s:4:"䕡";s:3:"䕡";s:4:"𦾱";s:4:"𦾱";s:4:"𧃒";s:4:"𧃒";s:4:"䕫";s:3:"䕫";s:4:"虐";s:3:"虐";s:4:"虜";s:3:"虜";s:4:"虧";s:3:"虧";s:4:"虩";s:3:"虩";s:4:"蚩";s:3:"蚩";s:4:"蚈";s:3:"蚈";s:4:"蜎";s:3:"蜎";s:4:"蛢";s:3:"蛢";s:4:"蝹";s:3:"蝹";s:4:"蜨";s:3:"蜨";s:4:"蝫";s:3:"蝫";s:4:"螆";s:3:"螆";s:4:"䗗";s:3:"䗗";s:4:"蟡";s:3:"蟡";s:4:"蠁";s:3:"蠁";s:4:"䗹";s:3:"䗹";s:4:"衠";s:3:"衠";s:4:"衣";s:3:"衣";s:4:"𧙧";s:4:"𧙧";s:4:"裗";s:3:"裗";s:4:"裞";s:3:"裞";s:4:"䘵";s:3:"䘵";s:4:"裺";s:3:"裺";s:4:"㒻";s:3:"㒻";s:4:"𧢮";s:4:"𧢮";s:4:"𧥦";s:4:"𧥦";s:4:"䚾";s:3:"䚾";s:4:"䛇";s:3:"䛇";s:4:"誠";s:3:"誠";s:4:"諭";s:3:"諭";s:4:"變";s:3:"變";s:4:"豕";s:3:"豕";s:4:"𧲨";s:4:"𧲨";s:4:"貫";s:3:"貫";s:4:"賁";s:3:"賁";s:4:"贛";s:3:"贛";s:4:"起";s:3:"起";s:4:"𧼯";s:4:"𧼯";s:4:"𠠄";s:4:"𠠄";s:4:"跋";s:3:"跋";s:4:"趼";s:3:"趼";s:4:"跰";s:3:"跰";s:4:"𠣞";s:4:"𠣞";s:4:"軔";s:3:"軔";s:4:"輸";s:3:"輸";s:4:"𨗒";s:4:"𨗒";s:4:"𨗭";s:4:"𨗭";s:4:"邔";s:3:"邔";s:4:"郱";s:3:"郱";s:4:"鄑";s:3:"鄑";s:4:"𨜮";s:4:"𨜮";s:4:"鄛";s:3:"鄛";s:4:"鈸";s:3:"鈸";s:4:"鋗";s:3:"鋗";s:4:"鋘";s:3:"鋘";s:4:"鉼";s:3:"鉼";s:4:"鏹";s:3:"鏹";s:4:"鐕";s:3:"鐕";s:4:"𨯺";s:4:"𨯺";s:4:"開";s:3:"開";s:4:"䦕";s:3:"䦕";s:4:"閷";s:3:"閷";s:4:"𨵷";s:4:"𨵷";s:4:"䧦";s:3:"䧦";s:4:"雃";s:3:"雃";s:4:"嶲";s:3:"嶲";s:4:"霣";s:3:"霣";s:4:"𩅅";s:4:"𩅅";s:4:"𩈚";s:4:"𩈚";s:4:"䩮";s:3:"䩮";s:4:"䩶";s:3:"䩶";s:4:"韠";s:3:"韠";s:4:"𩐊";s:4:"𩐊";s:4:"䪲";s:3:"䪲";s:4:"𩒖";s:4:"𩒖";s:4:"頋";s:3:"頋";s:4:"頋";s:3:"頋";s:4:"頩";s:3:"頩";s:4:"𩖶";s:4:"𩖶";s:4:"飢";s:3:"飢";s:4:"䬳";s:3:"䬳";s:4:"餩";s:3:"餩";s:4:"馧";s:3:"馧";s:4:"駂";s:3:"駂";s:4:"駾";s:3:"駾";s:4:"䯎";s:3:"䯎";s:4:"𩬰";s:4:"𩬰";s:4:"鬒";s:3:"鬒";s:4:"鱀";s:3:"鱀";s:4:"鳽";s:3:"鳽";s:4:"䳎";s:3:"䳎";s:4:"䳭";s:3:"䳭";s:4:"鵧";s:3:"鵧";s:4:"𪃎";s:4:"𪃎";s:4:"䳸";s:3:"䳸";s:4:"𪄅";s:4:"𪄅";s:4:"𪈎";s:4:"𪈎";s:4:"𪊑";s:4:"𪊑";s:4:"麻";s:3:"麻";s:4:"䵖";s:3:"䵖";s:4:"黹";s:3:"黹";s:4:"黾";s:3:"黾";s:4:"鼅";s:3:"鼅";s:4:"鼏";s:3:"鼏";s:4:"鼖";s:3:"鼖";s:4:"鼻";s:3:"鼻";s:4:"𪘀";s:4:"𪘀";}' );
$utfCheckNFC = unserialize( 'a:1217:{s:2:"̀";s:1:"N";s:2:"́";s:1:"N";s:2:"̓";s:1:"N";s:2:"̈́";s:1:"N";s:2:"ʹ";s:1:"N";s:2:";";s:1:"N";s:2:"·";s:1:"N";s:3:"क़";s:1:"N";s:3:"ख़";s:1:"N";s:3:"ग़";s:1:"N";s:3:"ज़";s:1:"N";s:3:"ड़";s:1:"N";s:3:"ढ़";s:1:"N";s:3:"फ़";s:1:"N";s:3:"य़";s:1:"N";s:3:"ড়";s:1:"N";s:3:"ঢ়";s:1:"N";s:3:"য়";s:1:"N";s:3:"ਲ਼";s:1:"N";s:3:"ਸ਼";s:1:"N";s:3:"ਖ਼";s:1:"N";s:3:"ਗ਼";s:1:"N";s:3:"ਜ਼";s:1:"N";s:3:"ਫ਼";s:1:"N";s:3:"ଡ଼";s:1:"N";s:3:"ଢ଼";s:1:"N";s:3:"གྷ";s:1:"N";s:3:"ཌྷ";s:1:"N";s:3:"དྷ";s:1:"N";s:3:"བྷ";s:1:"N";s:3:"ཛྷ";s:1:"N";s:3:"ཀྵ";s:1:"N";s:3:"ཱི";s:1:"N";s:3:"ཱུ";s:1:"N";s:3:"ྲྀ";s:1:"N";s:3:"ླྀ";s:1:"N";s:3:"ཱྀ";s:1:"N";s:3:"ྒྷ";s:1:"N";s:3:"ྜྷ";s:1:"N";s:3:"ྡྷ";s:1:"N";s:3:"ྦྷ";s:1:"N";s:3:"ྫྷ";s:1:"N";s:3:"ྐྵ";s:1:"N";s:3:"ά";s:1:"N";s:3:"έ";s:1:"N";s:3:"ή";s:1:"N";s:3:"ί";s:1:"N";s:3:"ό";s:1:"N";s:3:"ύ";s:1:"N";s:3:"ώ";s:1:"N";s:3:"Ά";s:1:"N";s:3:"ι";s:1:"N";s:3:"Έ";s:1:"N";s:3:"Ή";s:1:"N";s:3:"ΐ";s:1:"N";s:3:"Ί";s:1:"N";s:3:"ΰ";s:1:"N";s:3:"Ύ";s:1:"N";s:3:"΅";s:1:"N";s:3:"`";s:1:"N";s:3:"Ό";s:1:"N";s:3:"Ώ";s:1:"N";s:3:"´";s:1:"N";s:3:" ";s:1:"N";s:3:" ";s:1:"N";s:3:"Ω";s:1:"N";s:3:"K";s:1:"N";s:3:"Å";s:1:"N";s:3:"〈";s:1:"N";s:3:"〉";s:1:"N";s:3:"⫝̸";s:1:"N";s:3:"豈";s:1:"N";s:3:"更";s:1:"N";s:3:"車";s:1:"N";s:3:"賈";s:1:"N";s:3:"滑";s:1:"N";s:3:"串";s:1:"N";s:3:"句";s:1:"N";s:3:"龜";s:1:"N";s:3:"龜";s:1:"N";s:3:"契";s:1:"N";s:3:"金";s:1:"N";s:3:"喇";s:1:"N";s:3:"奈";s:1:"N";s:3:"懶";s:1:"N";s:3:"癩";s:1:"N";s:3:"羅";s:1:"N";s:3:"蘿";s:1:"N";s:3:"螺";s:1:"N";s:3:"裸";s:1:"N";s:3:"邏";s:1:"N";s:3:"樂";s:1:"N";s:3:"洛";s:1:"N";s:3:"烙";s:1:"N";s:3:"珞";s:1:"N";s:3:"落";s:1:"N";s:3:"酪";s:1:"N";s:3:"駱";s:1:"N";s:3:"亂";s:1:"N";s:3:"卵";s:1:"N";s:3:"欄";s:1:"N";s:3:"爛";s:1:"N";s:3:"蘭";s:1:"N";s:3:"鸞";s:1:"N";s:3:"嵐";s:1:"N";s:3:"濫";s:1:"N";s:3:"藍";s:1:"N";s:3:"襤";s:1:"N";s:3:"拉";s:1:"N";s:3:"臘";s:1:"N";s:3:"蠟";s:1:"N";s:3:"廊";s:1:"N";s:3:"朗";s:1:"N";s:3:"浪";s:1:"N";s:3:"狼";s:1:"N";s:3:"郎";s:1:"N";s:3:"來";s:1:"N";s:3:"冷";s:1:"N";s:3:"勞";s:1:"N";s:3:"擄";s:1:"N";s:3:"櫓";s:1:"N";s:3:"爐";s:1:"N";s:3:"盧";s:1:"N";s:3:"老";s:1:"N";s:3:"蘆";s:1:"N";s:3:"虜";s:1:"N";s:3:"路";s:1:"N";s:3:"露";s:1:"N";s:3:"魯";s:1:"N";s:3:"鷺";s:1:"N";s:3:"碌";s:1:"N";s:3:"祿";s:1:"N";s:3:"綠";s:1:"N";s:3:"菉";s:1:"N";s:3:"錄";s:1:"N";s:3:"鹿";s:1:"N";s:3:"論";s:1:"N";s:3:"壟";s:1:"N";s:3:"弄";s:1:"N";s:3:"籠";s:1:"N";s:3:"聾";s:1:"N";s:3:"牢";s:1:"N";s:3:"磊";s:1:"N";s:3:"賂";s:1:"N";s:3:"雷";s:1:"N";s:3:"壘";s:1:"N";s:3:"屢";s:1:"N";s:3:"樓";s:1:"N";s:3:"淚";s:1:"N";s:3:"漏";s:1:"N";s:3:"累";s:1:"N";s:3:"縷";s:1:"N";s:3:"陋";s:1:"N";s:3:"勒";s:1:"N";s:3:"肋";s:1:"N";s:3:"凜";s:1:"N";s:3:"凌";s:1:"N";s:3:"稜";s:1:"N";s:3:"綾";s:1:"N";s:3:"菱";s:1:"N";s:3:"陵";s:1:"N";s:3:"讀";s:1:"N";s:3:"拏";s:1:"N";s:3:"樂";s:1:"N";s:3:"諾";s:1:"N";s:3:"丹";s:1:"N";s:3:"寧";s:1:"N";s:3:"怒";s:1:"N";s:3:"率";s:1:"N";s:3:"異";s:1:"N";s:3:"北";s:1:"N";s:3:"磻";s:1:"N";s:3:"便";s:1:"N";s:3:"復";s:1:"N";s:3:"不";s:1:"N";s:3:"泌";s:1:"N";s:3:"數";s:1:"N";s:3:"索";s:1:"N";s:3:"參";s:1:"N";s:3:"塞";s:1:"N";s:3:"省";s:1:"N";s:3:"葉";s:1:"N";s:3:"說";s:1:"N";s:3:"殺";s:1:"N";s:3:"辰";s:1:"N";s:3:"沈";s:1:"N";s:3:"拾";s:1:"N";s:3:"若";s:1:"N";s:3:"掠";s:1:"N";s:3:"略";s:1:"N";s:3:"亮";s:1:"N";s:3:"兩";s:1:"N";s:3:"凉";s:1:"N";s:3:"梁";s:1:"N";s:3:"糧";s:1:"N";s:3:"良";s:1:"N";s:3:"諒";s:1:"N";s:3:"量";s:1:"N";s:3:"勵";s:1:"N";s:3:"呂";s:1:"N";s:3:"女";s:1:"N";s:3:"廬";s:1:"N";s:3:"旅";s:1:"N";s:3:"濾";s:1:"N";s:3:"礪";s:1:"N";s:3:"閭";s:1:"N";s:3:"驪";s:1:"N";s:3:"麗";s:1:"N";s:3:"黎";s:1:"N";s:3:"力";s:1:"N";s:3:"曆";s:1:"N";s:3:"歷";s:1:"N";s:3:"轢";s:1:"N";s:3:"年";s:1:"N";s:3:"憐";s:1:"N";s:3:"戀";s:1:"N";s:3:"撚";s:1:"N";s:3:"漣";s:1:"N";s:3:"煉";s:1:"N";s:3:"璉";s:1:"N";s:3:"秊";s:1:"N";s:3:"練";s:1:"N";s:3:"聯";s:1:"N";s:3:"輦";s:1:"N";s:3:"蓮";s:1:"N";s:3:"連";s:1:"N";s:3:"鍊";s:1:"N";s:3:"列";s:1:"N";s:3:"劣";s:1:"N";s:3:"咽";s:1:"N";s:3:"烈";s:1:"N";s:3:"裂";s:1:"N";s:3:"說";s:1:"N";s:3:"廉";s:1:"N";s:3:"念";s:1:"N";s:3:"捻";s:1:"N";s:3:"殮";s:1:"N";s:3:"簾";s:1:"N";s:3:"獵";s:1:"N";s:3:"令";s:1:"N";s:3:"囹";s:1:"N";s:3:"寧";s:1:"N";s:3:"嶺";s:1:"N";s:3:"怜";s:1:"N";s:3:"玲";s:1:"N";s:3:"瑩";s:1:"N";s:3:"羚";s:1:"N";s:3:"聆";s:1:"N";s:3:"鈴";s:1:"N";s:3:"零";s:1:"N";s:3:"靈";s:1:"N";s:3:"領";s:1:"N";s:3:"例";s:1:"N";s:3:"禮";s:1:"N";s:3:"醴";s:1:"N";s:3:"隸";s:1:"N";s:3:"惡";s:1:"N";s:3:"了";s:1:"N";s:3:"僚";s:1:"N";s:3:"寮";s:1:"N";s:3:"尿";s:1:"N";s:3:"料";s:1:"N";s:3:"樂";s:1:"N";s:3:"燎";s:1:"N";s:3:"療";s:1:"N";s:3:"蓼";s:1:"N";s:3:"遼";s:1:"N";s:3:"龍";s:1:"N";s:3:"暈";s:1:"N";s:3:"阮";s:1:"N";s:3:"劉";s:1:"N";s:3:"杻";s:1:"N";s:3:"柳";s:1:"N";s:3:"流";s:1:"N";s:3:"溜";s:1:"N";s:3:"琉";s:1:"N";s:3:"留";s:1:"N";s:3:"硫";s:1:"N";s:3:"紐";s:1:"N";s:3:"類";s:1:"N";s:3:"六";s:1:"N";s:3:"戮";s:1:"N";s:3:"陸";s:1:"N";s:3:"倫";s:1:"N";s:3:"崙";s:1:"N";s:3:"淪";s:1:"N";s:3:"輪";s:1:"N";s:3:"律";s:1:"N";s:3:"慄";s:1:"N";s:3:"栗";s:1:"N";s:3:"率";s:1:"N";s:3:"隆";s:1:"N";s:3:"利";s:1:"N";s:3:"吏";s:1:"N";s:3:"履";s:1:"N";s:3:"易";s:1:"N";s:3:"李";s:1:"N";s:3:"梨";s:1:"N";s:3:"泥";s:1:"N";s:3:"理";s:1:"N";s:3:"痢";s:1:"N";s:3:"罹";s:1:"N";s:3:"裏";s:1:"N";s:3:"裡";s:1:"N";s:3:"里";s:1:"N";s:3:"離";s:1:"N";s:3:"匿";s:1:"N";s:3:"溺";s:1:"N";s:3:"吝";s:1:"N";s:3:"燐";s:1:"N";s:3:"璘";s:1:"N";s:3:"藺";s:1:"N";s:3:"隣";s:1:"N";s:3:"鱗";s:1:"N";s:3:"麟";s:1:"N";s:3:"林";s:1:"N";s:3:"淋";s:1:"N";s:3:"臨";s:1:"N";s:3:"立";s:1:"N";s:3:"笠";s:1:"N";s:3:"粒";s:1:"N";s:3:"狀";s:1:"N";s:3:"炙";s:1:"N";s:3:"識";s:1:"N";s:3:"什";s:1:"N";s:3:"茶";s:1:"N";s:3:"刺";s:1:"N";s:3:"切";s:1:"N";s:3:"度";s:1:"N";s:3:"拓";s:1:"N";s:3:"糖";s:1:"N";s:3:"宅";s:1:"N";s:3:"洞";s:1:"N";s:3:"暴";s:1:"N";s:3:"輻";s:1:"N";s:3:"行";s:1:"N";s:3:"降";s:1:"N";s:3:"見";s:1:"N";s:3:"廓";s:1:"N";s:3:"兀";s:1:"N";s:3:"嗀";s:1:"N";s:3:"塚";s:1:"N";s:3:"晴";s:1:"N";s:3:"凞";s:1:"N";s:3:"猪";s:1:"N";s:3:"益";s:1:"N";s:3:"礼";s:1:"N";s:3:"神";s:1:"N";s:3:"祥";s:1:"N";s:3:"福";s:1:"N";s:3:"靖";s:1:"N";s:3:"精";s:1:"N";s:3:"羽";s:1:"N";s:3:"蘒";s:1:"N";s:3:"諸";s:1:"N";s:3:"逸";s:1:"N";s:3:"都";s:1:"N";s:3:"飯";s:1:"N";s:3:"飼";s:1:"N";s:3:"館";s:1:"N";s:3:"鶴";s:1:"N";s:3:"侮";s:1:"N";s:3:"僧";s:1:"N";s:3:"免";s:1:"N";s:3:"勉";s:1:"N";s:3:"勤";s:1:"N";s:3:"卑";s:1:"N";s:3:"喝";s:1:"N";s:3:"嘆";s:1:"N";s:3:"器";s:1:"N";s:3:"塀";s:1:"N";s:3:"墨";s:1:"N";s:3:"層";s:1:"N";s:3:"屮";s:1:"N";s:3:"悔";s:1:"N";s:3:"慨";s:1:"N";s:3:"憎";s:1:"N";s:3:"懲";s:1:"N";s:3:"敏";s:1:"N";s:3:"既";s:1:"N";s:3:"暑";s:1:"N";s:3:"梅";s:1:"N";s:3:"海";s:1:"N";s:3:"渚";s:1:"N";s:3:"漢";s:1:"N";s:3:"煮";s:1:"N";s:3:"爫";s:1:"N";s:3:"琢";s:1:"N";s:3:"碑";s:1:"N";s:3:"社";s:1:"N";s:3:"祉";s:1:"N";s:3:"祈";s:1:"N";s:3:"祐";s:1:"N";s:3:"祖";s:1:"N";s:3:"祝";s:1:"N";s:3:"禍";s:1:"N";s:3:"禎";s:1:"N";s:3:"穀";s:1:"N";s:3:"突";s:1:"N";s:3:"節";s:1:"N";s:3:"練";s:1:"N";s:3:"縉";s:1:"N";s:3:"繁";s:1:"N";s:3:"署";s:1:"N";s:3:"者";s:1:"N";s:3:"臭";s:1:"N";s:3:"艹";s:1:"N";s:3:"艹";s:1:"N";s:3:"著";s:1:"N";s:3:"褐";s:1:"N";s:3:"視";s:1:"N";s:3:"謁";s:1:"N";s:3:"謹";s:1:"N";s:3:"賓";s:1:"N";s:3:"贈";s:1:"N";s:3:"辶";s:1:"N";s:3:"逸";s:1:"N";s:3:"難";s:1:"N";s:3:"響";s:1:"N";s:3:"頻";s:1:"N";s:3:"並";s:1:"N";s:3:"况";s:1:"N";s:3:"全";s:1:"N";s:3:"侀";s:1:"N";s:3:"充";s:1:"N";s:3:"冀";s:1:"N";s:3:"勇";s:1:"N";s:3:"勺";s:1:"N";s:3:"喝";s:1:"N";s:3:"啕";s:1:"N";s:3:"喙";s:1:"N";s:3:"嗢";s:1:"N";s:3:"塚";s:1:"N";s:3:"墳";s:1:"N";s:3:"奄";s:1:"N";s:3:"奔";s:1:"N";s:3:"婢";s:1:"N";s:3:"嬨";s:1:"N";s:3:"廒";s:1:"N";s:3:"廙";s:1:"N";s:3:"彩";s:1:"N";s:3:"徭";s:1:"N";s:3:"惘";s:1:"N";s:3:"慎";s:1:"N";s:3:"愈";s:1:"N";s:3:"憎";s:1:"N";s:3:"慠";s:1:"N";s:3:"懲";s:1:"N";s:3:"戴";s:1:"N";s:3:"揄";s:1:"N";s:3:"搜";s:1:"N";s:3:"摒";s:1:"N";s:3:"敖";s:1:"N";s:3:"晴";s:1:"N";s:3:"朗";s:1:"N";s:3:"望";s:1:"N";s:3:"杖";s:1:"N";s:3:"歹";s:1:"N";s:3:"殺";s:1:"N";s:3:"流";s:1:"N";s:3:"滛";s:1:"N";s:3:"滋";s:1:"N";s:3:"漢";s:1:"N";s:3:"瀞";s:1:"N";s:3:"煮";s:1:"N";s:3:"瞧";s:1:"N";s:3:"爵";s:1:"N";s:3:"犯";s:1:"N";s:3:"猪";s:1:"N";s:3:"瑱";s:1:"N";s:3:"甆";s:1:"N";s:3:"画";s:1:"N";s:3:"瘝";s:1:"N";s:3:"瘟";s:1:"N";s:3:"益";s:1:"N";s:3:"盛";s:1:"N";s:3:"直";s:1:"N";s:3:"睊";s:1:"N";s:3:"着";s:1:"N";s:3:"磌";s:1:"N";s:3:"窱";s:1:"N";s:3:"節";s:1:"N";s:3:"类";s:1:"N";s:3:"絛";s:1:"N";s:3:"練";s:1:"N";s:3:"缾";s:1:"N";s:3:"者";s:1:"N";s:3:"荒";s:1:"N";s:3:"華";s:1:"N";s:3:"蝹";s:1:"N";s:3:"襁";s:1:"N";s:3:"覆";s:1:"N";s:3:"視";s:1:"N";s:3:"調";s:1:"N";s:3:"諸";s:1:"N";s:3:"請";s:1:"N";s:3:"謁";s:1:"N";s:3:"諾";s:1:"N";s:3:"諭";s:1:"N";s:3:"謹";s:1:"N";s:3:"變";s:1:"N";s:3:"贈";s:1:"N";s:3:"輸";s:1:"N";s:3:"遲";s:1:"N";s:3:"醙";s:1:"N";s:3:"鉶";s:1:"N";s:3:"陼";s:1:"N";s:3:"難";s:1:"N";s:3:"靖";s:1:"N";s:3:"韛";s:1:"N";s:3:"響";s:1:"N";s:3:"頋";s:1:"N";s:3:"頻";s:1:"N";s:3:"鬒";s:1:"N";s:3:"龜";s:1:"N";s:3:"𢡊";s:1:"N";s:3:"𢡄";s:1:"N";s:3:"𣏕";s:1:"N";s:3:"㮝";s:1:"N";s:3:"䀘";s:1:"N";s:3:"䀹";s:1:"N";s:3:"𥉉";s:1:"N";s:3:"𥳐";s:1:"N";s:3:"𧻓";s:1:"N";s:3:"齃";s:1:"N";s:3:"龎";s:1:"N";s:3:"יִ";s:1:"N";s:3:"ײַ";s:1:"N";s:3:"שׁ";s:1:"N";s:3:"שׂ";s:1:"N";s:3:"שּׁ";s:1:"N";s:3:"שּׂ";s:1:"N";s:3:"אַ";s:1:"N";s:3:"אָ";s:1:"N";s:3:"אּ";s:1:"N";s:3:"בּ";s:1:"N";s:3:"גּ";s:1:"N";s:3:"דּ";s:1:"N";s:3:"הּ";s:1:"N";s:3:"וּ";s:1:"N";s:3:"זּ";s:1:"N";s:3:"טּ";s:1:"N";s:3:"יּ";s:1:"N";s:3:"ךּ";s:1:"N";s:3:"כּ";s:1:"N";s:3:"לּ";s:1:"N";s:3:"מּ";s:1:"N";s:3:"נּ";s:1:"N";s:3:"סּ";s:1:"N";s:3:"ףּ";s:1:"N";s:3:"פּ";s:1:"N";s:3:"צּ";s:1:"N";s:3:"קּ";s:1:"N";s:3:"רּ";s:1:"N";s:3:"שּ";s:1:"N";s:3:"תּ";s:1:"N";s:3:"וֹ";s:1:"N";s:3:"בֿ";s:1:"N";s:3:"כֿ";s:1:"N";s:3:"פֿ";s:1:"N";s:4:"𝅗𝅥";s:1:"N";s:4:"𝅘𝅥";s:1:"N";s:4:"𝅘𝅥𝅮";s:1:"N";s:4:"𝅘𝅥𝅯";s:1:"N";s:4:"𝅘𝅥𝅰";s:1:"N";s:4:"𝅘𝅥𝅱";s:1:"N";s:4:"𝅘𝅥𝅲";s:1:"N";s:4:"𝆹𝅥";s:1:"N";s:4:"𝆺𝅥";s:1:"N";s:4:"𝆹𝅥𝅮";s:1:"N";s:4:"𝆺𝅥𝅮";s:1:"N";s:4:"𝆹𝅥𝅯";s:1:"N";s:4:"𝆺𝅥𝅯";s:1:"N";s:4:"丽";s:1:"N";s:4:"丸";s:1:"N";s:4:"乁";s:1:"N";s:4:"𠄢";s:1:"N";s:4:"你";s:1:"N";s:4:"侮";s:1:"N";s:4:"侻";s:1:"N";s:4:"倂";s:1:"N";s:4:"偺";s:1:"N";s:4:"備";s:1:"N";s:4:"僧";s:1:"N";s:4:"像";s:1:"N";s:4:"㒞";s:1:"N";s:4:"𠘺";s:1:"N";s:4:"免";s:1:"N";s:4:"兔";s:1:"N";s:4:"兤";s:1:"N";s:4:"具";s:1:"N";s:4:"𠔜";s:1:"N";s:4:"㒹";s:1:"N";s:4:"內";s:1:"N";s:4:"再";s:1:"N";s:4:"𠕋";s:1:"N";s:4:"冗";s:1:"N";s:4:"冤";s:1:"N";s:4:"仌";s:1:"N";s:4:"冬";s:1:"N";s:4:"况";s:1:"N";s:4:"𩇟";s:1:"N";s:4:"凵";s:1:"N";s:4:"刃";s:1:"N";s:4:"㓟";s:1:"N";s:4:"刻";s:1:"N";s:4:"剆";s:1:"N";s:4:"割";s:1:"N";s:4:"剷";s:1:"N";s:4:"㔕";s:1:"N";s:4:"勇";s:1:"N";s:4:"勉";s:1:"N";s:4:"勤";s:1:"N";s:4:"勺";s:1:"N";s:4:"包";s:1:"N";s:4:"匆";s:1:"N";s:4:"北";s:1:"N";s:4:"卉";s:1:"N";s:4:"卑";s:1:"N";s:4:"博";s:1:"N";s:4:"即";s:1:"N";s:4:"卽";s:1:"N";s:4:"卿";s:1:"N";s:4:"卿";s:1:"N";s:4:"卿";s:1:"N";s:4:"𠨬";s:1:"N";s:4:"灰";s:1:"N";s:4:"及";s:1:"N";s:4:"叟";s:1:"N";s:4:"𠭣";s:1:"N";s:4:"叫";s:1:"N";s:4:"叱";s:1:"N";s:4:"吆";s:1:"N";s:4:"咞";s:1:"N";s:4:"吸";s:1:"N";s:4:"呈";s:1:"N";s:4:"周";s:1:"N";s:4:"咢";s:1:"N";s:4:"哶";s:1:"N";s:4:"唐";s:1:"N";s:4:"啓";s:1:"N";s:4:"啣";s:1:"N";s:4:"善";s:1:"N";s:4:"善";s:1:"N";s:4:"喙";s:1:"N";s:4:"喫";s:1:"N";s:4:"喳";s:1:"N";s:4:"嗂";s:1:"N";s:4:"圖";s:1:"N";s:4:"嘆";s:1:"N";s:4:"圗";s:1:"N";s:4:"噑";s:1:"N";s:4:"噴";s:1:"N";s:4:"切";s:1:"N";s:4:"壮";s:1:"N";s:4:"城";s:1:"N";s:4:"埴";s:1:"N";s:4:"堍";s:1:"N";s:4:"型";s:1:"N";s:4:"堲";s:1:"N";s:4:"報";s:1:"N";s:4:"墬";s:1:"N";s:4:"𡓤";s:1:"N";s:4:"売";s:1:"N";s:4:"壷";s:1:"N";s:4:"夆";s:1:"N";s:4:"多";s:1:"N";s:4:"夢";s:1:"N";s:4:"奢";s:1:"N";s:4:"𡚨";s:1:"N";s:4:"𡛪";s:1:"N";s:4:"姬";s:1:"N";s:4:"娛";s:1:"N";s:4:"娧";s:1:"N";s:4:"姘";s:1:"N";s:4:"婦";s:1:"N";s:4:"㛮";s:1:"N";s:4:"㛼";s:1:"N";s:4:"嬈";s:1:"N";s:4:"嬾";s:1:"N";s:4:"嬾";s:1:"N";s:4:"𡧈";s:1:"N";s:4:"寃";s:1:"N";s:4:"寘";s:1:"N";s:4:"寧";s:1:"N";s:4:"寳";s:1:"N";s:4:"𡬘";s:1:"N";s:4:"寿";s:1:"N";s:4:"将";s:1:"N";s:4:"当";s:1:"N";s:4:"尢";s:1:"N";s:4:"㞁";s:1:"N";s:4:"屠";s:1:"N";s:4:"屮";s:1:"N";s:4:"峀";s:1:"N";s:4:"岍";s:1:"N";s:4:"𡷤";s:1:"N";s:4:"嵃";s:1:"N";s:4:"𡷦";s:1:"N";s:4:"嵮";s:1:"N";s:4:"嵫";s:1:"N";s:4:"嵼";s:1:"N";s:4:"巡";s:1:"N";s:4:"巢";s:1:"N";s:4:"㠯";s:1:"N";s:4:"巽";s:1:"N";s:4:"帨";s:1:"N";s:4:"帽";s:1:"N";s:4:"幩";s:1:"N";s:4:"㡢";s:1:"N";s:4:"𢆃";s:1:"N";s:4:"㡼";s:1:"N";s:4:"庰";s:1:"N";s:4:"庳";s:1:"N";s:4:"庶";s:1:"N";s:4:"廊";s:1:"N";s:4:"𪎒";s:1:"N";s:4:"廾";s:1:"N";s:4:"𢌱";s:1:"N";s:4:"𢌱";s:1:"N";s:4:"舁";s:1:"N";s:4:"弢";s:1:"N";s:4:"弢";s:1:"N";s:4:"㣇";s:1:"N";s:4:"𣊸";s:1:"N";s:4:"𦇚";s:1:"N";s:4:"形";s:1:"N";s:4:"彫";s:1:"N";s:4:"㣣";s:1:"N";s:4:"徚";s:1:"N";s:4:"忍";s:1:"N";s:4:"志";s:1:"N";s:4:"忹";s:1:"N";s:4:"悁";s:1:"N";s:4:"㤺";s:1:"N";s:4:"㤜";s:1:"N";s:4:"悔";s:1:"N";s:4:"𢛔";s:1:"N";s:4:"惇";s:1:"N";s:4:"慈";s:1:"N";s:4:"慌";s:1:"N";s:4:"慎";s:1:"N";s:4:"慌";s:1:"N";s:4:"慺";s:1:"N";s:4:"憎";s:1:"N";s:4:"憲";s:1:"N";s:4:"憤";s:1:"N";s:4:"憯";s:1:"N";s:4:"懞";s:1:"N";s:4:"懲";s:1:"N";s:4:"懶";s:1:"N";s:4:"成";s:1:"N";s:4:"戛";s:1:"N";s:4:"扝";s:1:"N";s:4:"抱";s:1:"N";s:4:"拔";s:1:"N";s:4:"捐";s:1:"N";s:4:"𢬌";s:1:"N";s:4:"挽";s:1:"N";s:4:"拼";s:1:"N";s:4:"捨";s:1:"N";s:4:"掃";s:1:"N";s:4:"揤";s:1:"N";s:4:"𢯱";s:1:"N";s:4:"搢";s:1:"N";s:4:"揅";s:1:"N";s:4:"掩";s:1:"N";s:4:"㨮";s:1:"N";s:4:"摩";s:1:"N";s:4:"摾";s:1:"N";s:4:"撝";s:1:"N";s:4:"摷";s:1:"N";s:4:"㩬";s:1:"N";s:4:"敏";s:1:"N";s:4:"敬";s:1:"N";s:4:"𣀊";s:1:"N";s:4:"旣";s:1:"N";s:4:"書";s:1:"N";s:4:"晉";s:1:"N";s:4:"㬙";s:1:"N";s:4:"暑";s:1:"N";s:4:"㬈";s:1:"N";s:4:"㫤";s:1:"N";s:4:"冒";s:1:"N";s:4:"冕";s:1:"N";s:4:"最";s:1:"N";s:4:"暜";s:1:"N";s:4:"肭";s:1:"N";s:4:"䏙";s:1:"N";s:4:"朗";s:1:"N";s:4:"望";s:1:"N";s:4:"朡";s:1:"N";s:4:"杞";s:1:"N";s:4:"杓";s:1:"N";s:4:"𣏃";s:1:"N";s:4:"㭉";s:1:"N";s:4:"柺";s:1:"N";s:4:"枅";s:1:"N";s:4:"桒";s:1:"N";s:4:"梅";s:1:"N";s:4:"𣑭";s:1:"N";s:4:"梎";s:1:"N";s:4:"栟";s:1:"N";s:4:"椔";s:1:"N";s:4:"㮝";s:1:"N";s:4:"楂";s:1:"N";s:4:"榣";s:1:"N";s:4:"槪";s:1:"N";s:4:"檨";s:1:"N";s:4:"𣚣";s:1:"N";s:4:"櫛";s:1:"N";s:4:"㰘";s:1:"N";s:4:"次";s:1:"N";s:4:"𣢧";s:1:"N";s:4:"歔";s:1:"N";s:4:"㱎";s:1:"N";s:4:"歲";s:1:"N";s:4:"殟";s:1:"N";s:4:"殺";s:1:"N";s:4:"殻";s:1:"N";s:4:"𣪍";s:1:"N";s:4:"𡴋";s:1:"N";s:4:"𣫺";s:1:"N";s:4:"汎";s:1:"N";s:4:"𣲼";s:1:"N";s:4:"沿";s:1:"N";s:4:"泍";s:1:"N";s:4:"汧";s:1:"N";s:4:"洖";s:1:"N";s:4:"派";s:1:"N";s:4:"海";s:1:"N";s:4:"流";s:1:"N";s:4:"浩";s:1:"N";s:4:"浸";s:1:"N";s:4:"涅";s:1:"N";s:4:"𣴞";s:1:"N";s:4:"洴";s:1:"N";s:4:"港";s:1:"N";s:4:"湮";s:1:"N";s:4:"㴳";s:1:"N";s:4:"滋";s:1:"N";s:4:"滇";s:1:"N";s:4:"𣻑";s:1:"N";s:4:"淹";s:1:"N";s:4:"潮";s:1:"N";s:4:"𣽞";s:1:"N";s:4:"𣾎";s:1:"N";s:4:"濆";s:1:"N";s:4:"瀹";s:1:"N";s:4:"瀞";s:1:"N";s:4:"瀛";s:1:"N";s:4:"㶖";s:1:"N";s:4:"灊";s:1:"N";s:4:"災";s:1:"N";s:4:"灷";s:1:"N";s:4:"炭";s:1:"N";s:4:"𠔥";s:1:"N";s:4:"煅";s:1:"N";s:4:"𤉣";s:1:"N";s:4:"熜";s:1:"N";s:4:"𤎫";s:1:"N";s:4:"爨";s:1:"N";s:4:"爵";s:1:"N";s:4:"牐";s:1:"N";s:4:"𤘈";s:1:"N";s:4:"犀";s:1:"N";s:4:"犕";s:1:"N";s:4:"𤜵";s:1:"N";s:4:"𤠔";s:1:"N";s:4:"獺";s:1:"N";s:4:"王";s:1:"N";s:4:"㺬";s:1:"N";s:4:"玥";s:1:"N";s:4:"㺸";s:1:"N";s:4:"㺸";s:1:"N";s:4:"瑇";s:1:"N";s:4:"瑜";s:1:"N";s:4:"瑱";s:1:"N";s:4:"璅";s:1:"N";s:4:"瓊";s:1:"N";s:4:"㼛";s:1:"N";s:4:"甤";s:1:"N";s:4:"𤰶";s:1:"N";s:4:"甾";s:1:"N";s:4:"𤲒";s:1:"N";s:4:"異";s:1:"N";s:4:"𢆟";s:1:"N";s:4:"瘐";s:1:"N";s:4:"𤾡";s:1:"N";s:4:"𤾸";s:1:"N";s:4:"𥁄";s:1:"N";s:4:"㿼";s:1:"N";s:4:"䀈";s:1:"N";s:4:"直";s:1:"N";s:4:"𥃳";s:1:"N";s:4:"𥃲";s:1:"N";s:4:"𥄙";s:1:"N";s:4:"𥄳";s:1:"N";s:4:"眞";s:1:"N";s:4:"真";s:1:"N";s:4:"真";s:1:"N";s:4:"睊";s:1:"N";s:4:"䀹";s:1:"N";s:4:"瞋";s:1:"N";s:4:"䁆";s:1:"N";s:4:"䂖";s:1:"N";s:4:"𥐝";s:1:"N";s:4:"硎";s:1:"N";s:4:"碌";s:1:"N";s:4:"磌";s:1:"N";s:4:"䃣";s:1:"N";s:4:"𥘦";s:1:"N";s:4:"祖";s:1:"N";s:4:"𥚚";s:1:"N";s:4:"𥛅";s:1:"N";s:4:"福";s:1:"N";s:4:"秫";s:1:"N";s:4:"䄯";s:1:"N";s:4:"穀";s:1:"N";s:4:"穊";s:1:"N";s:4:"穏";s:1:"N";s:4:"𥥼";s:1:"N";s:4:"𥪧";s:1:"N";s:4:"𥪧";s:1:"N";s:4:"竮";s:1:"N";s:4:"䈂";s:1:"N";s:4:"𥮫";s:1:"N";s:4:"篆";s:1:"N";s:4:"築";s:1:"N";s:4:"䈧";s:1:"N";s:4:"𥲀";s:1:"N";s:4:"糒";s:1:"N";s:4:"䊠";s:1:"N";s:4:"糨";s:1:"N";s:4:"糣";s:1:"N";s:4:"紀";s:1:"N";s:4:"𥾆";s:1:"N";s:4:"絣";s:1:"N";s:4:"䌁";s:1:"N";s:4:"緇";s:1:"N";s:4:"縂";s:1:"N";s:4:"繅";s:1:"N";s:4:"䌴";s:1:"N";s:4:"𦈨";s:1:"N";s:4:"𦉇";s:1:"N";s:4:"䍙";s:1:"N";s:4:"𦋙";s:1:"N";s:4:"罺";s:1:"N";s:4:"𦌾";s:1:"N";s:4:"羕";s:1:"N";s:4:"翺";s:1:"N";s:4:"者";s:1:"N";s:4:"𦓚";s:1:"N";s:4:"𦔣";s:1:"N";s:4:"聠";s:1:"N";s:4:"𦖨";s:1:"N";s:4:"聰";s:1:"N";s:4:"𣍟";s:1:"N";s:4:"䏕";s:1:"N";s:4:"育";s:1:"N";s:4:"脃";s:1:"N";s:4:"䐋";s:1:"N";s:4:"脾";s:1:"N";s:4:"媵";s:1:"N";s:4:"𦞧";s:1:"N";s:4:"𦞵";s:1:"N";s:4:"𣎓";s:1:"N";s:4:"𣎜";s:1:"N";s:4:"舁";s:1:"N";s:4:"舄";s:1:"N";s:4:"辞";s:1:"N";s:4:"䑫";s:1:"N";s:4:"芑";s:1:"N";s:4:"芋";s:1:"N";s:4:"芝";s:1:"N";s:4:"劳";s:1:"N";s:4:"花";s:1:"N";s:4:"芳";s:1:"N";s:4:"芽";s:1:"N";s:4:"苦";s:1:"N";s:4:"𦬼";s:1:"N";s:4:"若";s:1:"N";s:4:"茝";s:1:"N";s:4:"荣";s:1:"N";s:4:"莭";s:1:"N";s:4:"茣";s:1:"N";s:4:"莽";s:1:"N";s:4:"菧";s:1:"N";s:4:"著";s:1:"N";s:4:"荓";s:1:"N";s:4:"菊";s:1:"N";s:4:"菌";s:1:"N";s:4:"菜";s:1:"N";s:4:"𦰶";s:1:"N";s:4:"𦵫";s:1:"N";s:4:"𦳕";s:1:"N";s:4:"䔫";s:1:"N";s:4:"蓱";s:1:"N";s:4:"蓳";s:1:"N";s:4:"蔖";s:1:"N";s:4:"𧏊";s:1:"N";s:4:"蕤";s:1:"N";s:4:"𦼬";s:1:"N";s:4:"䕝";s:1:"N";s:4:"䕡";s:1:"N";s:4:"𦾱";s:1:"N";s:4:"𧃒";s:1:"N";s:4:"䕫";s:1:"N";s:4:"虐";s:1:"N";s:4:"虜";s:1:"N";s:4:"虧";s:1:"N";s:4:"虩";s:1:"N";s:4:"蚩";s:1:"N";s:4:"蚈";s:1:"N";s:4:"蜎";s:1:"N";s:4:"蛢";s:1:"N";s:4:"蝹";s:1:"N";s:4:"蜨";s:1:"N";s:4:"蝫";s:1:"N";s:4:"螆";s:1:"N";s:4:"䗗";s:1:"N";s:4:"蟡";s:1:"N";s:4:"蠁";s:1:"N";s:4:"䗹";s:1:"N";s:4:"衠";s:1:"N";s:4:"衣";s:1:"N";s:4:"𧙧";s:1:"N";s:4:"裗";s:1:"N";s:4:"裞";s:1:"N";s:4:"䘵";s:1:"N";s:4:"裺";s:1:"N";s:4:"㒻";s:1:"N";s:4:"𧢮";s:1:"N";s:4:"𧥦";s:1:"N";s:4:"䚾";s:1:"N";s:4:"䛇";s:1:"N";s:4:"誠";s:1:"N";s:4:"諭";s:1:"N";s:4:"變";s:1:"N";s:4:"豕";s:1:"N";s:4:"𧲨";s:1:"N";s:4:"貫";s:1:"N";s:4:"賁";s:1:"N";s:4:"贛";s:1:"N";s:4:"起";s:1:"N";s:4:"𧼯";s:1:"N";s:4:"𠠄";s:1:"N";s:4:"跋";s:1:"N";s:4:"趼";s:1:"N";s:4:"跰";s:1:"N";s:4:"𠣞";s:1:"N";s:4:"軔";s:1:"N";s:4:"輸";s:1:"N";s:4:"𨗒";s:1:"N";s:4:"𨗭";s:1:"N";s:4:"邔";s:1:"N";s:4:"郱";s:1:"N";s:4:"鄑";s:1:"N";s:4:"𨜮";s:1:"N";s:4:"鄛";s:1:"N";s:4:"鈸";s:1:"N";s:4:"鋗";s:1:"N";s:4:"鋘";s:1:"N";s:4:"鉼";s:1:"N";s:4:"鏹";s:1:"N";s:4:"鐕";s:1:"N";s:4:"𨯺";s:1:"N";s:4:"開";s:1:"N";s:4:"䦕";s:1:"N";s:4:"閷";s:1:"N";s:4:"𨵷";s:1:"N";s:4:"䧦";s:1:"N";s:4:"雃";s:1:"N";s:4:"嶲";s:1:"N";s:4:"霣";s:1:"N";s:4:"𩅅";s:1:"N";s:4:"𩈚";s:1:"N";s:4:"䩮";s:1:"N";s:4:"䩶";s:1:"N";s:4:"韠";s:1:"N";s:4:"𩐊";s:1:"N";s:4:"䪲";s:1:"N";s:4:"𩒖";s:1:"N";s:4:"頋";s:1:"N";s:4:"頋";s:1:"N";s:4:"頩";s:1:"N";s:4:"𩖶";s:1:"N";s:4:"飢";s:1:"N";s:4:"䬳";s:1:"N";s:4:"餩";s:1:"N";s:4:"馧";s:1:"N";s:4:"駂";s:1:"N";s:4:"駾";s:1:"N";s:4:"䯎";s:1:"N";s:4:"𩬰";s:1:"N";s:4:"鬒";s:1:"N";s:4:"鱀";s:1:"N";s:4:"鳽";s:1:"N";s:4:"䳎";s:1:"N";s:4:"䳭";s:1:"N";s:4:"鵧";s:1:"N";s:4:"𪃎";s:1:"N";s:4:"䳸";s:1:"N";s:4:"𪄅";s:1:"N";s:4:"𪈎";s:1:"N";s:4:"𪊑";s:1:"N";s:4:"麻";s:1:"N";s:4:"䵖";s:1:"N";s:4:"黹";s:1:"N";s:4:"黾";s:1:"N";s:4:"鼅";s:1:"N";s:4:"鼏";s:1:"N";s:4:"鼖";s:1:"N";s:4:"鼻";s:1:"N";s:4:"𪘀";s:1:"N";s:2:"̀";s:1:"M";s:2:"́";s:1:"M";s:2:"̂";s:1:"M";s:2:"̃";s:1:"M";s:2:"̄";s:1:"M";s:2:"̆";s:1:"M";s:2:"̇";s:1:"M";s:2:"̈";s:1:"M";s:2:"̉";s:1:"M";s:2:"̊";s:1:"M";s:2:"̋";s:1:"M";s:2:"̌";s:1:"M";s:2:"̏";s:1:"M";s:2:"̑";s:1:"M";s:2:"̓";s:1:"M";s:2:"̔";s:1:"M";s:2:"̛";s:1:"M";s:2:"̣";s:1:"M";s:2:"̤";s:1:"M";s:2:"̥";s:1:"M";s:2:"̦";s:1:"M";s:2:"̧";s:1:"M";s:2:"̨";s:1:"M";s:2:"̭";s:1:"M";s:2:"̮";s:1:"M";s:2:"̰";s:1:"M";s:2:"̱";s:1:"M";s:2:"̸";s:1:"M";s:2:"͂";s:1:"M";s:2:"ͅ";s:1:"M";s:2:"ٓ";s:1:"M";s:2:"ٔ";s:1:"M";s:2:"ٕ";s:1:"M";s:3:"़";s:1:"M";s:3:"া";s:1:"M";s:3:"ৗ";s:1:"M";s:3:"ା";s:1:"M";s:3:"ୖ";s:1:"M";s:3:"ୗ";s:1:"M";s:3:"ா";s:1:"M";s:3:"ௗ";s:1:"M";s:3:"ౖ";s:1:"M";s:3:"ೂ";s:1:"M";s:3:"ೕ";s:1:"M";s:3:"ೖ";s:1:"M";s:3:"ാ";s:1:"M";s:3:"ൗ";s:1:"M";s:3:"්";s:1:"M";s:3:"ා";s:1:"M";s:3:"ෟ";s:1:"M";s:3:"ီ";s:1:"M";s:3:"ᅡ";s:1:"M";s:3:"ᅢ";s:1:"M";s:3:"ᅣ";s:1:"M";s:3:"ᅤ";s:1:"M";s:3:"ᅥ";s:1:"M";s:3:"ᅦ";s:1:"M";s:3:"ᅧ";s:1:"M";s:3:"ᅨ";s:1:"M";s:3:"ᅩ";s:1:"M";s:3:"ᅪ";s:1:"M";s:3:"ᅫ";s:1:"M";s:3:"ᅬ";s:1:"M";s:3:"ᅭ";s:1:"M";s:3:"ᅮ";s:1:"M";s:3:"ᅯ";s:1:"M";s:3:"ᅰ";s:1:"M";s:3:"ᅱ";s:1:"M";s:3:"ᅲ";s:1:"M";s:3:"ᅳ";s:1:"M";s:3:"ᅴ";s:1:"M";s:3:"ᅵ";s:1:"M";s:3:"ᆨ";s:1:"M";s:3:"ᆩ";s:1:"M";s:3:"ᆪ";s:1:"M";s:3:"ᆫ";s:1:"M";s:3:"ᆬ";s:1:"M";s:3:"ᆭ";s:1:"M";s:3:"ᆮ";s:1:"M";s:3:"ᆯ";s:1:"M";s:3:"ᆰ";s:1:"M";s:3:"ᆱ";s:1:"M";s:3:"ᆲ";s:1:"M";s:3:"ᆳ";s:1:"M";s:3:"ᆴ";s:1:"M";s:3:"ᆵ";s:1:"M";s:3:"ᆶ";s:1:"M";s:3:"ᆷ";s:1:"M";s:3:"ᆸ";s:1:"M";s:3:"ᆹ";s:1:"M";s:3:"ᆺ";s:1:"M";s:3:"ᆻ";s:1:"M";s:3:"ᆼ";s:1:"M";s:3:"ᆽ";s:1:"M";s:3:"ᆾ";s:1:"M";s:3:"ᆿ";s:1:"M";s:3:"ᇀ";s:1:"M";s:3:"ᇁ";s:1:"M";s:3:"ᇂ";s:1:"M";s:3:"ᬵ";s:1:"M";s:3:"゙";s:1:"M";s:3:"゚";s:1:"M";}' );
diff --git a/includes/normal/UtfNormalDataK.inc b/includes/normal/UtfNormalDataK.inc
index 5f112e02..a97e005a 100644
--- a/includes/normal/UtfNormalDataK.inc
+++ b/includes/normal/UtfNormalDataK.inc
@@ -5,5 +5,5 @@
*/
/** */
global $utfCompatibilityDecomp;
-$utfCompatibilityDecomp = unserialize( 'a:5402:{s:2:" ";s:1:" ";s:2:"¨";s:3:" ̈";s:2:"ª";s:1:"a";s:2:"¯";s:3:" ̄";s:2:"²";s:1:"2";s:2:"³";s:1:"3";s:2:"´";s:3:" ́";s:2:"µ";s:2:"μ";s:2:"¸";s:3:" ̧";s:2:"¹";s:1:"1";s:2:"º";s:1:"o";s:2:"¼";s:5:"1⁄4";s:2:"½";s:5:"1⁄2";s:2:"¾";s:5:"3⁄4";s:2:"À";s:3:"À";s:2:"Á";s:3:"Á";s:2:"Â";s:3:"Â";s:2:"Ã";s:3:"Ã";s:2:"Ä";s:3:"Ä";s:2:"Å";s:3:"Å";s:2:"Ç";s:3:"Ç";s:2:"È";s:3:"È";s:2:"É";s:3:"É";s:2:"Ê";s:3:"Ê";s:2:"Ë";s:3:"Ë";s:2:"Ì";s:3:"Ì";s:2:"Í";s:3:"Í";s:2:"Î";s:3:"Î";s:2:"Ï";s:3:"Ï";s:2:"Ñ";s:3:"Ñ";s:2:"Ò";s:3:"Ò";s:2:"Ó";s:3:"Ó";s:2:"Ô";s:3:"Ô";s:2:"Õ";s:3:"Õ";s:2:"Ö";s:3:"Ö";s:2:"Ù";s:3:"Ù";s:2:"Ú";s:3:"Ú";s:2:"Û";s:3:"Û";s:2:"Ü";s:3:"Ü";s:2:"Ý";s:3:"Ý";s:2:"à";s:3:"à";s:2:"á";s:3:"á";s:2:"â";s:3:"â";s:2:"ã";s:3:"ã";s:2:"ä";s:3:"ä";s:2:"å";s:3:"å";s:2:"ç";s:3:"ç";s:2:"è";s:3:"è";s:2:"é";s:3:"é";s:2:"ê";s:3:"ê";s:2:"ë";s:3:"ë";s:2:"ì";s:3:"ì";s:2:"í";s:3:"í";s:2:"î";s:3:"î";s:2:"ï";s:3:"ï";s:2:"ñ";s:3:"ñ";s:2:"ò";s:3:"ò";s:2:"ó";s:3:"ó";s:2:"ô";s:3:"ô";s:2:"õ";s:3:"õ";s:2:"ö";s:3:"ö";s:2:"ù";s:3:"ù";s:2:"ú";s:3:"ú";s:2:"û";s:3:"û";s:2:"ü";s:3:"ü";s:2:"ý";s:3:"ý";s:2:"ÿ";s:3:"ÿ";s:2:"Ā";s:3:"Ā";s:2:"ā";s:3:"ā";s:2:"Ă";s:3:"Ă";s:2:"ă";s:3:"ă";s:2:"Ą";s:3:"Ą";s:2:"ą";s:3:"ą";s:2:"Ć";s:3:"Ć";s:2:"ć";s:3:"ć";s:2:"Ĉ";s:3:"Ĉ";s:2:"ĉ";s:3:"ĉ";s:2:"Ċ";s:3:"Ċ";s:2:"ċ";s:3:"ċ";s:2:"Č";s:3:"Č";s:2:"č";s:3:"č";s:2:"Ď";s:3:"Ď";s:2:"ď";s:3:"ď";s:2:"Ē";s:3:"Ē";s:2:"ē";s:3:"ē";s:2:"Ĕ";s:3:"Ĕ";s:2:"ĕ";s:3:"ĕ";s:2:"Ė";s:3:"Ė";s:2:"ė";s:3:"ė";s:2:"Ę";s:3:"Ę";s:2:"ę";s:3:"ę";s:2:"Ě";s:3:"Ě";s:2:"ě";s:3:"ě";s:2:"Ĝ";s:3:"Ĝ";s:2:"ĝ";s:3:"ĝ";s:2:"Ğ";s:3:"Ğ";s:2:"ğ";s:3:"ğ";s:2:"Ġ";s:3:"Ġ";s:2:"ġ";s:3:"ġ";s:2:"Ģ";s:3:"Ģ";s:2:"ģ";s:3:"ģ";s:2:"Ĥ";s:3:"Ĥ";s:2:"ĥ";s:3:"ĥ";s:2:"Ĩ";s:3:"Ĩ";s:2:"ĩ";s:3:"ĩ";s:2:"Ī";s:3:"Ī";s:2:"ī";s:3:"ī";s:2:"Ĭ";s:3:"Ĭ";s:2:"ĭ";s:3:"ĭ";s:2:"Į";s:3:"Į";s:2:"į";s:3:"į";s:2:"İ";s:3:"İ";s:2:"IJ";s:2:"IJ";s:2:"ij";s:2:"ij";s:2:"Ĵ";s:3:"Ĵ";s:2:"ĵ";s:3:"ĵ";s:2:"Ķ";s:3:"Ķ";s:2:"ķ";s:3:"ķ";s:2:"Ĺ";s:3:"Ĺ";s:2:"ĺ";s:3:"ĺ";s:2:"Ļ";s:3:"Ļ";s:2:"ļ";s:3:"ļ";s:2:"Ľ";s:3:"Ľ";s:2:"ľ";s:3:"ľ";s:2:"Ŀ";s:3:"L·";s:2:"ŀ";s:3:"l·";s:2:"Ń";s:3:"Ń";s:2:"ń";s:3:"ń";s:2:"Ņ";s:3:"Ņ";s:2:"ņ";s:3:"ņ";s:2:"Ň";s:3:"Ň";s:2:"ň";s:3:"ň";s:2:"ʼn";s:3:"ʼn";s:2:"Ō";s:3:"Ō";s:2:"ō";s:3:"ō";s:2:"Ŏ";s:3:"Ŏ";s:2:"ŏ";s:3:"ŏ";s:2:"Ő";s:3:"Ő";s:2:"ő";s:3:"ő";s:2:"Ŕ";s:3:"Ŕ";s:2:"ŕ";s:3:"ŕ";s:2:"Ŗ";s:3:"Ŗ";s:2:"ŗ";s:3:"ŗ";s:2:"Ř";s:3:"Ř";s:2:"ř";s:3:"ř";s:2:"Ś";s:3:"Ś";s:2:"ś";s:3:"ś";s:2:"Ŝ";s:3:"Ŝ";s:2:"ŝ";s:3:"ŝ";s:2:"Ş";s:3:"Ş";s:2:"ş";s:3:"ş";s:2:"Š";s:3:"Š";s:2:"š";s:3:"š";s:2:"Ţ";s:3:"Ţ";s:2:"ţ";s:3:"ţ";s:2:"Ť";s:3:"Ť";s:2:"ť";s:3:"ť";s:2:"Ũ";s:3:"Ũ";s:2:"ũ";s:3:"ũ";s:2:"Ū";s:3:"Ū";s:2:"ū";s:3:"ū";s:2:"Ŭ";s:3:"Ŭ";s:2:"ŭ";s:3:"ŭ";s:2:"Ů";s:3:"Ů";s:2:"ů";s:3:"ů";s:2:"Ű";s:3:"Ű";s:2:"ű";s:3:"ű";s:2:"Ų";s:3:"Ų";s:2:"ų";s:3:"ų";s:2:"Ŵ";s:3:"Ŵ";s:2:"ŵ";s:3:"ŵ";s:2:"Ŷ";s:3:"Ŷ";s:2:"ŷ";s:3:"ŷ";s:2:"Ÿ";s:3:"Ÿ";s:2:"Ź";s:3:"Ź";s:2:"ź";s:3:"ź";s:2:"Ż";s:3:"Ż";s:2:"ż";s:3:"ż";s:2:"Ž";s:3:"Ž";s:2:"ž";s:3:"ž";s:2:"ſ";s:1:"s";s:2:"Ơ";s:3:"Ơ";s:2:"ơ";s:3:"ơ";s:2:"Ư";s:3:"Ư";s:2:"ư";s:3:"ư";s:2:"DŽ";s:4:"DŽ";s:2:"Dž";s:4:"Dž";s:2:"dž";s:4:"dž";s:2:"LJ";s:2:"LJ";s:2:"Lj";s:2:"Lj";s:2:"lj";s:2:"lj";s:2:"NJ";s:2:"NJ";s:2:"Nj";s:2:"Nj";s:2:"nj";s:2:"nj";s:2:"Ǎ";s:3:"Ǎ";s:2:"ǎ";s:3:"ǎ";s:2:"Ǐ";s:3:"Ǐ";s:2:"ǐ";s:3:"ǐ";s:2:"Ǒ";s:3:"Ǒ";s:2:"ǒ";s:3:"ǒ";s:2:"Ǔ";s:3:"Ǔ";s:2:"ǔ";s:3:"ǔ";s:2:"Ǖ";s:5:"Ǖ";s:2:"ǖ";s:5:"ǖ";s:2:"Ǘ";s:5:"Ǘ";s:2:"ǘ";s:5:"ǘ";s:2:"Ǚ";s:5:"Ǚ";s:2:"ǚ";s:5:"ǚ";s:2:"Ǜ";s:5:"Ǜ";s:2:"ǜ";s:5:"ǜ";s:2:"Ǟ";s:5:"Ǟ";s:2:"ǟ";s:5:"ǟ";s:2:"Ǡ";s:5:"Ǡ";s:2:"ǡ";s:5:"ǡ";s:2:"Ǣ";s:4:"Ǣ";s:2:"ǣ";s:4:"ǣ";s:2:"Ǧ";s:3:"Ǧ";s:2:"ǧ";s:3:"ǧ";s:2:"Ǩ";s:3:"Ǩ";s:2:"ǩ";s:3:"ǩ";s:2:"Ǫ";s:3:"Ǫ";s:2:"ǫ";s:3:"ǫ";s:2:"Ǭ";s:5:"Ǭ";s:2:"ǭ";s:5:"ǭ";s:2:"Ǯ";s:4:"Ǯ";s:2:"ǯ";s:4:"ǯ";s:2:"ǰ";s:3:"ǰ";s:2:"DZ";s:2:"DZ";s:2:"Dz";s:2:"Dz";s:2:"dz";s:2:"dz";s:2:"Ǵ";s:3:"Ǵ";s:2:"ǵ";s:3:"ǵ";s:2:"Ǹ";s:3:"Ǹ";s:2:"ǹ";s:3:"ǹ";s:2:"Ǻ";s:5:"Ǻ";s:2:"ǻ";s:5:"ǻ";s:2:"Ǽ";s:4:"Ǽ";s:2:"ǽ";s:4:"ǽ";s:2:"Ǿ";s:4:"Ǿ";s:2:"ǿ";s:4:"ǿ";s:2:"Ȁ";s:3:"Ȁ";s:2:"ȁ";s:3:"ȁ";s:2:"Ȃ";s:3:"Ȃ";s:2:"ȃ";s:3:"ȃ";s:2:"Ȅ";s:3:"Ȅ";s:2:"ȅ";s:3:"ȅ";s:2:"Ȇ";s:3:"Ȇ";s:2:"ȇ";s:3:"ȇ";s:2:"Ȉ";s:3:"Ȉ";s:2:"ȉ";s:3:"ȉ";s:2:"Ȋ";s:3:"Ȋ";s:2:"ȋ";s:3:"ȋ";s:2:"Ȍ";s:3:"Ȍ";s:2:"ȍ";s:3:"ȍ";s:2:"Ȏ";s:3:"Ȏ";s:2:"ȏ";s:3:"ȏ";s:2:"Ȑ";s:3:"Ȑ";s:2:"ȑ";s:3:"ȑ";s:2:"Ȓ";s:3:"Ȓ";s:2:"ȓ";s:3:"ȓ";s:2:"Ȕ";s:3:"Ȕ";s:2:"ȕ";s:3:"ȕ";s:2:"Ȗ";s:3:"Ȗ";s:2:"ȗ";s:3:"ȗ";s:2:"Ș";s:3:"Ș";s:2:"ș";s:3:"ș";s:2:"Ț";s:3:"Ț";s:2:"ț";s:3:"ț";s:2:"Ȟ";s:3:"Ȟ";s:2:"ȟ";s:3:"ȟ";s:2:"Ȧ";s:3:"Ȧ";s:2:"ȧ";s:3:"ȧ";s:2:"Ȩ";s:3:"Ȩ";s:2:"ȩ";s:3:"ȩ";s:2:"Ȫ";s:5:"Ȫ";s:2:"ȫ";s:5:"ȫ";s:2:"Ȭ";s:5:"Ȭ";s:2:"ȭ";s:5:"ȭ";s:2:"Ȯ";s:3:"Ȯ";s:2:"ȯ";s:3:"ȯ";s:2:"Ȱ";s:5:"Ȱ";s:2:"ȱ";s:5:"ȱ";s:2:"Ȳ";s:3:"Ȳ";s:2:"ȳ";s:3:"ȳ";s:2:"ʰ";s:1:"h";s:2:"ʱ";s:2:"ɦ";s:2:"ʲ";s:1:"j";s:2:"ʳ";s:1:"r";s:2:"ʴ";s:2:"ɹ";s:2:"ʵ";s:2:"ɻ";s:2:"ʶ";s:2:"ʁ";s:2:"ʷ";s:1:"w";s:2:"ʸ";s:1:"y";s:2:"˘";s:3:" ̆";s:2:"˙";s:3:" ̇";s:2:"˚";s:3:" ̊";s:2:"˛";s:3:" ̨";s:2:"˜";s:3:" ̃";s:2:"˝";s:3:" ̋";s:2:"ˠ";s:2:"ɣ";s:2:"ˡ";s:1:"l";s:2:"ˢ";s:1:"s";s:2:"ˣ";s:1:"x";s:2:"ˤ";s:2:"ʕ";s:2:"̀";s:2:"̀";s:2:"́";s:2:"́";s:2:"̓";s:2:"̓";s:2:"̈́";s:4:"̈́";s:2:"ʹ";s:2:"ʹ";s:2:"ͺ";s:3:" ͅ";s:2:";";s:1:";";s:2:"΄";s:3:" ́";s:2:"΅";s:5:" ̈́";s:2:"Ά";s:4:"Ά";s:2:"·";s:2:"·";s:2:"Έ";s:4:"Έ";s:2:"Ή";s:4:"Ή";s:2:"Ί";s:4:"Ί";s:2:"Ό";s:4:"Ό";s:2:"Ύ";s:4:"Ύ";s:2:"Ώ";s:4:"Ώ";s:2:"ΐ";s:6:"ΐ";s:2:"Ϊ";s:4:"Ϊ";s:2:"Ϋ";s:4:"Ϋ";s:2:"ά";s:4:"ά";s:2:"έ";s:4:"έ";s:2:"ή";s:4:"ή";s:2:"ί";s:4:"ί";s:2:"ΰ";s:6:"ΰ";s:2:"ϊ";s:4:"ϊ";s:2:"ϋ";s:4:"ϋ";s:2:"ό";s:4:"ό";s:2:"ύ";s:4:"ύ";s:2:"ώ";s:4:"ώ";s:2:"ϐ";s:2:"β";s:2:"ϑ";s:2:"θ";s:2:"ϒ";s:2:"Υ";s:2:"ϓ";s:4:"Ύ";s:2:"ϔ";s:4:"Ϋ";s:2:"ϕ";s:2:"φ";s:2:"ϖ";s:2:"π";s:2:"ϰ";s:2:"κ";s:2:"ϱ";s:2:"ρ";s:2:"ϲ";s:2:"ς";s:2:"ϴ";s:2:"Θ";s:2:"ϵ";s:2:"ε";s:2:"Ϲ";s:2:"Σ";s:2:"Ѐ";s:4:"Ѐ";s:2:"Ё";s:4:"Ё";s:2:"Ѓ";s:4:"Ѓ";s:2:"Ї";s:4:"Ї";s:2:"Ќ";s:4:"Ќ";s:2:"Ѝ";s:4:"Ѝ";s:2:"Ў";s:4:"Ў";s:2:"Й";s:4:"Й";s:2:"й";s:4:"й";s:2:"ѐ";s:4:"ѐ";s:2:"ё";s:4:"ё";s:2:"ѓ";s:4:"ѓ";s:2:"ї";s:4:"ї";s:2:"ќ";s:4:"ќ";s:2:"ѝ";s:4:"ѝ";s:2:"ў";s:4:"ў";s:2:"Ѷ";s:4:"Ѷ";s:2:"ѷ";s:4:"ѷ";s:2:"Ӂ";s:4:"Ӂ";s:2:"ӂ";s:4:"ӂ";s:2:"Ӑ";s:4:"Ӑ";s:2:"ӑ";s:4:"ӑ";s:2:"Ӓ";s:4:"Ӓ";s:2:"ӓ";s:4:"ӓ";s:2:"Ӗ";s:4:"Ӗ";s:2:"ӗ";s:4:"ӗ";s:2:"Ӛ";s:4:"Ӛ";s:2:"ӛ";s:4:"ӛ";s:2:"Ӝ";s:4:"Ӝ";s:2:"ӝ";s:4:"ӝ";s:2:"Ӟ";s:4:"Ӟ";s:2:"ӟ";s:4:"ӟ";s:2:"Ӣ";s:4:"Ӣ";s:2:"ӣ";s:4:"ӣ";s:2:"Ӥ";s:4:"Ӥ";s:2:"ӥ";s:4:"ӥ";s:2:"Ӧ";s:4:"Ӧ";s:2:"ӧ";s:4:"ӧ";s:2:"Ӫ";s:4:"Ӫ";s:2:"ӫ";s:4:"ӫ";s:2:"Ӭ";s:4:"Ӭ";s:2:"ӭ";s:4:"ӭ";s:2:"Ӯ";s:4:"Ӯ";s:2:"ӯ";s:4:"ӯ";s:2:"Ӱ";s:4:"Ӱ";s:2:"ӱ";s:4:"ӱ";s:2:"Ӳ";s:4:"Ӳ";s:2:"ӳ";s:4:"ӳ";s:2:"Ӵ";s:4:"Ӵ";s:2:"ӵ";s:4:"ӵ";s:2:"Ӹ";s:4:"Ӹ";s:2:"ӹ";s:4:"ӹ";s:2:"և";s:4:"եւ";s:2:"آ";s:4:"آ";s:2:"أ";s:4:"أ";s:2:"ؤ";s:4:"ؤ";s:2:"إ";s:4:"إ";s:2:"ئ";s:4:"ئ";s:2:"ٵ";s:4:"اٴ";s:2:"ٶ";s:4:"وٴ";s:2:"ٷ";s:4:"ۇٴ";s:2:"ٸ";s:4:"يٴ";s:2:"ۀ";s:4:"ۀ";s:2:"ۂ";s:4:"ۂ";s:2:"ۓ";s:4:"ۓ";s:3:"ऩ";s:6:"ऩ";s:3:"ऱ";s:6:"ऱ";s:3:"ऴ";s:6:"ऴ";s:3:"क़";s:6:"क़";s:3:"ख़";s:6:"ख़";s:3:"ग़";s:6:"ग़";s:3:"ज़";s:6:"ज़";s:3:"ड़";s:6:"ड़";s:3:"ढ़";s:6:"ढ़";s:3:"फ़";s:6:"फ़";s:3:"य़";s:6:"य़";s:3:"ো";s:6:"ো";s:3:"ৌ";s:6:"ৌ";s:3:"ড়";s:6:"ড়";s:3:"ঢ়";s:6:"ঢ়";s:3:"য়";s:6:"য়";s:3:"ਲ਼";s:6:"ਲ਼";s:3:"ਸ਼";s:6:"ਸ਼";s:3:"ਖ਼";s:6:"ਖ਼";s:3:"ਗ਼";s:6:"ਗ਼";s:3:"ਜ਼";s:6:"ਜ਼";s:3:"ਫ਼";s:6:"ਫ਼";s:3:"ୈ";s:6:"ୈ";s:3:"ୋ";s:6:"ୋ";s:3:"ୌ";s:6:"ୌ";s:3:"ଡ଼";s:6:"ଡ଼";s:3:"ଢ଼";s:6:"ଢ଼";s:3:"ஔ";s:6:"ஔ";s:3:"ொ";s:6:"ொ";s:3:"ோ";s:6:"ோ";s:3:"ௌ";s:6:"ௌ";s:3:"ై";s:6:"ై";s:3:"ೀ";s:6:"ೀ";s:3:"ೇ";s:6:"ೇ";s:3:"ೈ";s:6:"ೈ";s:3:"ೊ";s:6:"ೊ";s:3:"ೋ";s:9:"ೋ";s:3:"ൊ";s:6:"ൊ";s:3:"ോ";s:6:"ോ";s:3:"ൌ";s:6:"ൌ";s:3:"ේ";s:6:"ේ";s:3:"ො";s:6:"ො";s:3:"ෝ";s:9:"ෝ";s:3:"ෞ";s:6:"ෞ";s:3:"ำ";s:6:"ํา";s:3:"ຳ";s:6:"ໍາ";s:3:"ໜ";s:6:"ຫນ";s:3:"ໝ";s:6:"ຫມ";s:3:"༌";s:3:"་";s:3:"གྷ";s:6:"གྷ";s:3:"ཌྷ";s:6:"ཌྷ";s:3:"དྷ";s:6:"དྷ";s:3:"བྷ";s:6:"བྷ";s:3:"ཛྷ";s:6:"ཛྷ";s:3:"ཀྵ";s:6:"ཀྵ";s:3:"ཱི";s:6:"ཱི";s:3:"ཱུ";s:6:"ཱུ";s:3:"ྲྀ";s:6:"ྲྀ";s:3:"ཷ";s:9:"ྲཱྀ";s:3:"ླྀ";s:6:"ླྀ";s:3:"ཹ";s:9:"ླཱྀ";s:3:"ཱྀ";s:6:"ཱྀ";s:3:"ྒྷ";s:6:"ྒྷ";s:3:"ྜྷ";s:6:"ྜྷ";s:3:"ྡྷ";s:6:"ྡྷ";s:3:"ྦྷ";s:6:"ྦྷ";s:3:"ྫྷ";s:6:"ྫྷ";s:3:"ྐྵ";s:6:"ྐྵ";s:3:"ဦ";s:6:"ဦ";s:3:"ჼ";s:3:"ნ";s:3:"ᬆ";s:6:"ᬆ";s:3:"ᬈ";s:6:"ᬈ";s:3:"ᬊ";s:6:"ᬊ";s:3:"ᬌ";s:6:"ᬌ";s:3:"ᬎ";s:6:"ᬎ";s:3:"ᬒ";s:6:"ᬒ";s:3:"ᬻ";s:6:"ᬻ";s:3:"ᬽ";s:6:"ᬽ";s:3:"ᭀ";s:6:"ᭀ";s:3:"ᭁ";s:6:"ᭁ";s:3:"ᭃ";s:6:"ᭃ";s:3:"ᴬ";s:1:"A";s:3:"ᴭ";s:2:"Æ";s:3:"ᴮ";s:1:"B";s:3:"ᴰ";s:1:"D";s:3:"ᴱ";s:1:"E";s:3:"ᴲ";s:2:"Ǝ";s:3:"ᴳ";s:1:"G";s:3:"ᴴ";s:1:"H";s:3:"ᴵ";s:1:"I";s:3:"ᴶ";s:1:"J";s:3:"ᴷ";s:1:"K";s:3:"ᴸ";s:1:"L";s:3:"ᴹ";s:1:"M";s:3:"ᴺ";s:1:"N";s:3:"ᴼ";s:1:"O";s:3:"ᴽ";s:2:"Ȣ";s:3:"ᴾ";s:1:"P";s:3:"ᴿ";s:1:"R";s:3:"ᵀ";s:1:"T";s:3:"ᵁ";s:1:"U";s:3:"ᵂ";s:1:"W";s:3:"ᵃ";s:1:"a";s:3:"ᵄ";s:2:"ɐ";s:3:"ᵅ";s:2:"ɑ";s:3:"ᵆ";s:3:"ᴂ";s:3:"ᵇ";s:1:"b";s:3:"ᵈ";s:1:"d";s:3:"ᵉ";s:1:"e";s:3:"ᵊ";s:2:"ə";s:3:"ᵋ";s:2:"ɛ";s:3:"ᵌ";s:2:"ɜ";s:3:"ᵍ";s:1:"g";s:3:"ᵏ";s:1:"k";s:3:"ᵐ";s:1:"m";s:3:"ᵑ";s:2:"ŋ";s:3:"ᵒ";s:1:"o";s:3:"ᵓ";s:2:"ɔ";s:3:"ᵔ";s:3:"ᴖ";s:3:"ᵕ";s:3:"ᴗ";s:3:"ᵖ";s:1:"p";s:3:"ᵗ";s:1:"t";s:3:"ᵘ";s:1:"u";s:3:"ᵙ";s:3:"ᴝ";s:3:"ᵚ";s:2:"ɯ";s:3:"ᵛ";s:1:"v";s:3:"ᵜ";s:3:"ᴥ";s:3:"ᵝ";s:2:"β";s:3:"ᵞ";s:2:"γ";s:3:"ᵟ";s:2:"δ";s:3:"ᵠ";s:2:"φ";s:3:"ᵡ";s:2:"χ";s:3:"ᵢ";s:1:"i";s:3:"ᵣ";s:1:"r";s:3:"ᵤ";s:1:"u";s:3:"ᵥ";s:1:"v";s:3:"ᵦ";s:2:"β";s:3:"ᵧ";s:2:"γ";s:3:"ᵨ";s:2:"ρ";s:3:"ᵩ";s:2:"φ";s:3:"ᵪ";s:2:"χ";s:3:"ᵸ";s:2:"н";s:3:"ᶛ";s:2:"ɒ";s:3:"ᶜ";s:1:"c";s:3:"ᶝ";s:2:"ɕ";s:3:"ᶞ";s:2:"ð";s:3:"ᶟ";s:2:"ɜ";s:3:"ᶠ";s:1:"f";s:3:"ᶡ";s:2:"ɟ";s:3:"ᶢ";s:2:"ɡ";s:3:"ᶣ";s:2:"ɥ";s:3:"ᶤ";s:2:"ɨ";s:3:"ᶥ";s:2:"ɩ";s:3:"ᶦ";s:2:"ɪ";s:3:"ᶧ";s:3:"ᵻ";s:3:"ᶨ";s:2:"ʝ";s:3:"ᶩ";s:2:"ɭ";s:3:"ᶪ";s:3:"ᶅ";s:3:"ᶫ";s:2:"ʟ";s:3:"ᶬ";s:2:"ɱ";s:3:"ᶭ";s:2:"ɰ";s:3:"ᶮ";s:2:"ɲ";s:3:"ᶯ";s:2:"ɳ";s:3:"ᶰ";s:2:"ɴ";s:3:"ᶱ";s:2:"ɵ";s:3:"ᶲ";s:2:"ɸ";s:3:"ᶳ";s:2:"ʂ";s:3:"ᶴ";s:2:"ʃ";s:3:"ᶵ";s:2:"ƫ";s:3:"ᶶ";s:2:"ʉ";s:3:"ᶷ";s:2:"ʊ";s:3:"ᶸ";s:3:"ᴜ";s:3:"ᶹ";s:2:"ʋ";s:3:"ᶺ";s:2:"ʌ";s:3:"ᶻ";s:1:"z";s:3:"ᶼ";s:2:"ʐ";s:3:"ᶽ";s:2:"ʑ";s:3:"ᶾ";s:2:"ʒ";s:3:"ᶿ";s:2:"θ";s:3:"Ḁ";s:3:"Ḁ";s:3:"ḁ";s:3:"ḁ";s:3:"Ḃ";s:3:"Ḃ";s:3:"ḃ";s:3:"ḃ";s:3:"Ḅ";s:3:"Ḅ";s:3:"ḅ";s:3:"ḅ";s:3:"Ḇ";s:3:"Ḇ";s:3:"ḇ";s:3:"ḇ";s:3:"Ḉ";s:5:"Ḉ";s:3:"ḉ";s:5:"ḉ";s:3:"Ḋ";s:3:"Ḋ";s:3:"ḋ";s:3:"ḋ";s:3:"Ḍ";s:3:"Ḍ";s:3:"ḍ";s:3:"ḍ";s:3:"Ḏ";s:3:"Ḏ";s:3:"ḏ";s:3:"ḏ";s:3:"Ḑ";s:3:"Ḑ";s:3:"ḑ";s:3:"ḑ";s:3:"Ḓ";s:3:"Ḓ";s:3:"ḓ";s:3:"ḓ";s:3:"Ḕ";s:5:"Ḕ";s:3:"ḕ";s:5:"ḕ";s:3:"Ḗ";s:5:"Ḗ";s:3:"ḗ";s:5:"ḗ";s:3:"Ḙ";s:3:"Ḙ";s:3:"ḙ";s:3:"ḙ";s:3:"Ḛ";s:3:"Ḛ";s:3:"ḛ";s:3:"ḛ";s:3:"Ḝ";s:5:"Ḝ";s:3:"ḝ";s:5:"ḝ";s:3:"Ḟ";s:3:"Ḟ";s:3:"ḟ";s:3:"ḟ";s:3:"Ḡ";s:3:"Ḡ";s:3:"ḡ";s:3:"ḡ";s:3:"Ḣ";s:3:"Ḣ";s:3:"ḣ";s:3:"ḣ";s:3:"Ḥ";s:3:"Ḥ";s:3:"ḥ";s:3:"ḥ";s:3:"Ḧ";s:3:"Ḧ";s:3:"ḧ";s:3:"ḧ";s:3:"Ḩ";s:3:"Ḩ";s:3:"ḩ";s:3:"ḩ";s:3:"Ḫ";s:3:"Ḫ";s:3:"ḫ";s:3:"ḫ";s:3:"Ḭ";s:3:"Ḭ";s:3:"ḭ";s:3:"ḭ";s:3:"Ḯ";s:5:"Ḯ";s:3:"ḯ";s:5:"ḯ";s:3:"Ḱ";s:3:"Ḱ";s:3:"ḱ";s:3:"ḱ";s:3:"Ḳ";s:3:"Ḳ";s:3:"ḳ";s:3:"ḳ";s:3:"Ḵ";s:3:"Ḵ";s:3:"ḵ";s:3:"ḵ";s:3:"Ḷ";s:3:"Ḷ";s:3:"ḷ";s:3:"ḷ";s:3:"Ḹ";s:5:"Ḹ";s:3:"ḹ";s:5:"ḹ";s:3:"Ḻ";s:3:"Ḻ";s:3:"ḻ";s:3:"ḻ";s:3:"Ḽ";s:3:"Ḽ";s:3:"ḽ";s:3:"ḽ";s:3:"Ḿ";s:3:"Ḿ";s:3:"ḿ";s:3:"ḿ";s:3:"Ṁ";s:3:"Ṁ";s:3:"ṁ";s:3:"ṁ";s:3:"Ṃ";s:3:"Ṃ";s:3:"ṃ";s:3:"ṃ";s:3:"Ṅ";s:3:"Ṅ";s:3:"ṅ";s:3:"ṅ";s:3:"Ṇ";s:3:"Ṇ";s:3:"ṇ";s:3:"ṇ";s:3:"Ṉ";s:3:"Ṉ";s:3:"ṉ";s:3:"ṉ";s:3:"Ṋ";s:3:"Ṋ";s:3:"ṋ";s:3:"ṋ";s:3:"Ṍ";s:5:"Ṍ";s:3:"ṍ";s:5:"ṍ";s:3:"Ṏ";s:5:"Ṏ";s:3:"ṏ";s:5:"ṏ";s:3:"Ṑ";s:5:"Ṑ";s:3:"ṑ";s:5:"ṑ";s:3:"Ṓ";s:5:"Ṓ";s:3:"ṓ";s:5:"ṓ";s:3:"Ṕ";s:3:"Ṕ";s:3:"ṕ";s:3:"ṕ";s:3:"Ṗ";s:3:"Ṗ";s:3:"ṗ";s:3:"ṗ";s:3:"Ṙ";s:3:"Ṙ";s:3:"ṙ";s:3:"ṙ";s:3:"Ṛ";s:3:"Ṛ";s:3:"ṛ";s:3:"ṛ";s:3:"Ṝ";s:5:"Ṝ";s:3:"ṝ";s:5:"ṝ";s:3:"Ṟ";s:3:"Ṟ";s:3:"ṟ";s:3:"ṟ";s:3:"Ṡ";s:3:"Ṡ";s:3:"ṡ";s:3:"ṡ";s:3:"Ṣ";s:3:"Ṣ";s:3:"ṣ";s:3:"ṣ";s:3:"Ṥ";s:5:"Ṥ";s:3:"ṥ";s:5:"ṥ";s:3:"Ṧ";s:5:"Ṧ";s:3:"ṧ";s:5:"ṧ";s:3:"Ṩ";s:5:"Ṩ";s:3:"ṩ";s:5:"ṩ";s:3:"Ṫ";s:3:"Ṫ";s:3:"ṫ";s:3:"ṫ";s:3:"Ṭ";s:3:"Ṭ";s:3:"ṭ";s:3:"ṭ";s:3:"Ṯ";s:3:"Ṯ";s:3:"ṯ";s:3:"ṯ";s:3:"Ṱ";s:3:"Ṱ";s:3:"ṱ";s:3:"ṱ";s:3:"Ṳ";s:3:"Ṳ";s:3:"ṳ";s:3:"ṳ";s:3:"Ṵ";s:3:"Ṵ";s:3:"ṵ";s:3:"ṵ";s:3:"Ṷ";s:3:"Ṷ";s:3:"ṷ";s:3:"ṷ";s:3:"Ṹ";s:5:"Ṹ";s:3:"ṹ";s:5:"ṹ";s:3:"Ṻ";s:5:"Ṻ";s:3:"ṻ";s:5:"ṻ";s:3:"Ṽ";s:3:"Ṽ";s:3:"ṽ";s:3:"ṽ";s:3:"Ṿ";s:3:"Ṿ";s:3:"ṿ";s:3:"ṿ";s:3:"Ẁ";s:3:"Ẁ";s:3:"ẁ";s:3:"ẁ";s:3:"Ẃ";s:3:"Ẃ";s:3:"ẃ";s:3:"ẃ";s:3:"Ẅ";s:3:"Ẅ";s:3:"ẅ";s:3:"ẅ";s:3:"Ẇ";s:3:"Ẇ";s:3:"ẇ";s:3:"ẇ";s:3:"Ẉ";s:3:"Ẉ";s:3:"ẉ";s:3:"ẉ";s:3:"Ẋ";s:3:"Ẋ";s:3:"ẋ";s:3:"ẋ";s:3:"Ẍ";s:3:"Ẍ";s:3:"ẍ";s:3:"ẍ";s:3:"Ẏ";s:3:"Ẏ";s:3:"ẏ";s:3:"ẏ";s:3:"Ẑ";s:3:"Ẑ";s:3:"ẑ";s:3:"ẑ";s:3:"Ẓ";s:3:"Ẓ";s:3:"ẓ";s:3:"ẓ";s:3:"Ẕ";s:3:"Ẕ";s:3:"ẕ";s:3:"ẕ";s:3:"ẖ";s:3:"ẖ";s:3:"ẗ";s:3:"ẗ";s:3:"ẘ";s:3:"ẘ";s:3:"ẙ";s:3:"ẙ";s:3:"ẚ";s:3:"aʾ";s:3:"ẛ";s:3:"ṡ";s:3:"Ạ";s:3:"Ạ";s:3:"ạ";s:3:"ạ";s:3:"Ả";s:3:"Ả";s:3:"ả";s:3:"ả";s:3:"Ấ";s:5:"Ấ";s:3:"ấ";s:5:"ấ";s:3:"Ầ";s:5:"Ầ";s:3:"ầ";s:5:"ầ";s:3:"Ẩ";s:5:"Ẩ";s:3:"ẩ";s:5:"ẩ";s:3:"Ẫ";s:5:"Ẫ";s:3:"ẫ";s:5:"ẫ";s:3:"Ậ";s:5:"Ậ";s:3:"ậ";s:5:"ậ";s:3:"Ắ";s:5:"Ắ";s:3:"ắ";s:5:"ắ";s:3:"Ằ";s:5:"Ằ";s:3:"ằ";s:5:"ằ";s:3:"Ẳ";s:5:"Ẳ";s:3:"ẳ";s:5:"ẳ";s:3:"Ẵ";s:5:"Ẵ";s:3:"ẵ";s:5:"ẵ";s:3:"Ặ";s:5:"Ặ";s:3:"ặ";s:5:"ặ";s:3:"Ẹ";s:3:"Ẹ";s:3:"ẹ";s:3:"ẹ";s:3:"Ẻ";s:3:"Ẻ";s:3:"ẻ";s:3:"ẻ";s:3:"Ẽ";s:3:"Ẽ";s:3:"ẽ";s:3:"ẽ";s:3:"Ế";s:5:"Ế";s:3:"ế";s:5:"ế";s:3:"Ề";s:5:"Ề";s:3:"ề";s:5:"ề";s:3:"Ể";s:5:"Ể";s:3:"ể";s:5:"ể";s:3:"Ễ";s:5:"Ễ";s:3:"ễ";s:5:"ễ";s:3:"Ệ";s:5:"Ệ";s:3:"ệ";s:5:"ệ";s:3:"Ỉ";s:3:"Ỉ";s:3:"ỉ";s:3:"ỉ";s:3:"Ị";s:3:"Ị";s:3:"ị";s:3:"ị";s:3:"Ọ";s:3:"Ọ";s:3:"ọ";s:3:"ọ";s:3:"Ỏ";s:3:"Ỏ";s:3:"ỏ";s:3:"ỏ";s:3:"Ố";s:5:"Ố";s:3:"ố";s:5:"ố";s:3:"Ồ";s:5:"Ồ";s:3:"ồ";s:5:"ồ";s:3:"Ổ";s:5:"Ổ";s:3:"ổ";s:5:"ổ";s:3:"Ỗ";s:5:"Ỗ";s:3:"ỗ";s:5:"ỗ";s:3:"Ộ";s:5:"Ộ";s:3:"ộ";s:5:"ộ";s:3:"Ớ";s:5:"Ớ";s:3:"ớ";s:5:"ớ";s:3:"Ờ";s:5:"Ờ";s:3:"ờ";s:5:"ờ";s:3:"Ở";s:5:"Ở";s:3:"ở";s:5:"ở";s:3:"Ỡ";s:5:"Ỡ";s:3:"ỡ";s:5:"ỡ";s:3:"Ợ";s:5:"Ợ";s:3:"ợ";s:5:"ợ";s:3:"Ụ";s:3:"Ụ";s:3:"ụ";s:3:"ụ";s:3:"Ủ";s:3:"Ủ";s:3:"ủ";s:3:"ủ";s:3:"Ứ";s:5:"Ứ";s:3:"ứ";s:5:"ứ";s:3:"Ừ";s:5:"Ừ";s:3:"ừ";s:5:"ừ";s:3:"Ử";s:5:"Ử";s:3:"ử";s:5:"ử";s:3:"Ữ";s:5:"Ữ";s:3:"ữ";s:5:"ữ";s:3:"Ự";s:5:"Ự";s:3:"ự";s:5:"ự";s:3:"Ỳ";s:3:"Ỳ";s:3:"ỳ";s:3:"ỳ";s:3:"Ỵ";s:3:"Ỵ";s:3:"ỵ";s:3:"ỵ";s:3:"Ỷ";s:3:"Ỷ";s:3:"ỷ";s:3:"ỷ";s:3:"Ỹ";s:3:"Ỹ";s:3:"ỹ";s:3:"ỹ";s:3:"ἀ";s:4:"ἀ";s:3:"ἁ";s:4:"ἁ";s:3:"ἂ";s:6:"ἂ";s:3:"ἃ";s:6:"ἃ";s:3:"ἄ";s:6:"ἄ";s:3:"ἅ";s:6:"ἅ";s:3:"ἆ";s:6:"ἆ";s:3:"ἇ";s:6:"ἇ";s:3:"Ἀ";s:4:"Ἀ";s:3:"Ἁ";s:4:"Ἁ";s:3:"Ἂ";s:6:"Ἂ";s:3:"Ἃ";s:6:"Ἃ";s:3:"Ἄ";s:6:"Ἄ";s:3:"Ἅ";s:6:"Ἅ";s:3:"Ἆ";s:6:"Ἆ";s:3:"Ἇ";s:6:"Ἇ";s:3:"ἐ";s:4:"ἐ";s:3:"ἑ";s:4:"ἑ";s:3:"ἒ";s:6:"ἒ";s:3:"ἓ";s:6:"ἓ";s:3:"ἔ";s:6:"ἔ";s:3:"ἕ";s:6:"ἕ";s:3:"Ἐ";s:4:"Ἐ";s:3:"Ἑ";s:4:"Ἑ";s:3:"Ἒ";s:6:"Ἒ";s:3:"Ἓ";s:6:"Ἓ";s:3:"Ἔ";s:6:"Ἔ";s:3:"Ἕ";s:6:"Ἕ";s:3:"ἠ";s:4:"ἠ";s:3:"ἡ";s:4:"ἡ";s:3:"ἢ";s:6:"ἢ";s:3:"ἣ";s:6:"ἣ";s:3:"ἤ";s:6:"ἤ";s:3:"ἥ";s:6:"ἥ";s:3:"ἦ";s:6:"ἦ";s:3:"ἧ";s:6:"ἧ";s:3:"Ἠ";s:4:"Ἠ";s:3:"Ἡ";s:4:"Ἡ";s:3:"Ἢ";s:6:"Ἢ";s:3:"Ἣ";s:6:"Ἣ";s:3:"Ἤ";s:6:"Ἤ";s:3:"Ἥ";s:6:"Ἥ";s:3:"Ἦ";s:6:"Ἦ";s:3:"Ἧ";s:6:"Ἧ";s:3:"ἰ";s:4:"ἰ";s:3:"ἱ";s:4:"ἱ";s:3:"ἲ";s:6:"ἲ";s:3:"ἳ";s:6:"ἳ";s:3:"ἴ";s:6:"ἴ";s:3:"ἵ";s:6:"ἵ";s:3:"ἶ";s:6:"ἶ";s:3:"ἷ";s:6:"ἷ";s:3:"Ἰ";s:4:"Ἰ";s:3:"Ἱ";s:4:"Ἱ";s:3:"Ἲ";s:6:"Ἲ";s:3:"Ἳ";s:6:"Ἳ";s:3:"Ἴ";s:6:"Ἴ";s:3:"Ἵ";s:6:"Ἵ";s:3:"Ἶ";s:6:"Ἶ";s:3:"Ἷ";s:6:"Ἷ";s:3:"ὀ";s:4:"ὀ";s:3:"ὁ";s:4:"ὁ";s:3:"ὂ";s:6:"ὂ";s:3:"ὃ";s:6:"ὃ";s:3:"ὄ";s:6:"ὄ";s:3:"ὅ";s:6:"ὅ";s:3:"Ὀ";s:4:"Ὀ";s:3:"Ὁ";s:4:"Ὁ";s:3:"Ὂ";s:6:"Ὂ";s:3:"Ὃ";s:6:"Ὃ";s:3:"Ὄ";s:6:"Ὄ";s:3:"Ὅ";s:6:"Ὅ";s:3:"ὐ";s:4:"ὐ";s:3:"ὑ";s:4:"ὑ";s:3:"ὒ";s:6:"ὒ";s:3:"ὓ";s:6:"ὓ";s:3:"ὔ";s:6:"ὔ";s:3:"ὕ";s:6:"ὕ";s:3:"ὖ";s:6:"ὖ";s:3:"ὗ";s:6:"ὗ";s:3:"Ὑ";s:4:"Ὑ";s:3:"Ὓ";s:6:"Ὓ";s:3:"Ὕ";s:6:"Ὕ";s:3:"Ὗ";s:6:"Ὗ";s:3:"ὠ";s:4:"ὠ";s:3:"ὡ";s:4:"ὡ";s:3:"ὢ";s:6:"ὢ";s:3:"ὣ";s:6:"ὣ";s:3:"ὤ";s:6:"ὤ";s:3:"ὥ";s:6:"ὥ";s:3:"ὦ";s:6:"ὦ";s:3:"ὧ";s:6:"ὧ";s:3:"Ὠ";s:4:"Ὠ";s:3:"Ὡ";s:4:"Ὡ";s:3:"Ὢ";s:6:"Ὢ";s:3:"Ὣ";s:6:"Ὣ";s:3:"Ὤ";s:6:"Ὤ";s:3:"Ὥ";s:6:"Ὥ";s:3:"Ὦ";s:6:"Ὦ";s:3:"Ὧ";s:6:"Ὧ";s:3:"ὰ";s:4:"ὰ";s:3:"ά";s:4:"ά";s:3:"ὲ";s:4:"ὲ";s:3:"έ";s:4:"έ";s:3:"ὴ";s:4:"ὴ";s:3:"ή";s:4:"ή";s:3:"ὶ";s:4:"ὶ";s:3:"ί";s:4:"ί";s:3:"ὸ";s:4:"ὸ";s:3:"ό";s:4:"ό";s:3:"ὺ";s:4:"ὺ";s:3:"ύ";s:4:"ύ";s:3:"ὼ";s:4:"ὼ";s:3:"ώ";s:4:"ώ";s:3:"ᾀ";s:6:"ᾀ";s:3:"ᾁ";s:6:"ᾁ";s:3:"ᾂ";s:8:"ᾂ";s:3:"ᾃ";s:8:"ᾃ";s:3:"ᾄ";s:8:"ᾄ";s:3:"ᾅ";s:8:"ᾅ";s:3:"ᾆ";s:8:"ᾆ";s:3:"ᾇ";s:8:"ᾇ";s:3:"ᾈ";s:6:"ᾈ";s:3:"ᾉ";s:6:"ᾉ";s:3:"ᾊ";s:8:"ᾊ";s:3:"ᾋ";s:8:"ᾋ";s:3:"ᾌ";s:8:"ᾌ";s:3:"ᾍ";s:8:"ᾍ";s:3:"ᾎ";s:8:"ᾎ";s:3:"ᾏ";s:8:"ᾏ";s:3:"ᾐ";s:6:"ᾐ";s:3:"ᾑ";s:6:"ᾑ";s:3:"ᾒ";s:8:"ᾒ";s:3:"ᾓ";s:8:"ᾓ";s:3:"ᾔ";s:8:"ᾔ";s:3:"ᾕ";s:8:"ᾕ";s:3:"ᾖ";s:8:"ᾖ";s:3:"ᾗ";s:8:"ᾗ";s:3:"ᾘ";s:6:"ᾘ";s:3:"ᾙ";s:6:"ᾙ";s:3:"ᾚ";s:8:"ᾚ";s:3:"ᾛ";s:8:"ᾛ";s:3:"ᾜ";s:8:"ᾜ";s:3:"ᾝ";s:8:"ᾝ";s:3:"ᾞ";s:8:"ᾞ";s:3:"ᾟ";s:8:"ᾟ";s:3:"ᾠ";s:6:"ᾠ";s:3:"ᾡ";s:6:"ᾡ";s:3:"ᾢ";s:8:"ᾢ";s:3:"ᾣ";s:8:"ᾣ";s:3:"ᾤ";s:8:"ᾤ";s:3:"ᾥ";s:8:"ᾥ";s:3:"ᾦ";s:8:"ᾦ";s:3:"ᾧ";s:8:"ᾧ";s:3:"ᾨ";s:6:"ᾨ";s:3:"ᾩ";s:6:"ᾩ";s:3:"ᾪ";s:8:"ᾪ";s:3:"ᾫ";s:8:"ᾫ";s:3:"ᾬ";s:8:"ᾬ";s:3:"ᾭ";s:8:"ᾭ";s:3:"ᾮ";s:8:"ᾮ";s:3:"ᾯ";s:8:"ᾯ";s:3:"ᾰ";s:4:"ᾰ";s:3:"ᾱ";s:4:"ᾱ";s:3:"ᾲ";s:6:"ᾲ";s:3:"ᾳ";s:4:"ᾳ";s:3:"ᾴ";s:6:"ᾴ";s:3:"ᾶ";s:4:"ᾶ";s:3:"ᾷ";s:6:"ᾷ";s:3:"Ᾰ";s:4:"Ᾰ";s:3:"Ᾱ";s:4:"Ᾱ";s:3:"Ὰ";s:4:"Ὰ";s:3:"Ά";s:4:"Ά";s:3:"ᾼ";s:4:"ᾼ";s:3:"᾽";s:3:" ̓";s:3:"ι";s:2:"ι";s:3:"᾿";s:3:" ̓";s:3:"῀";s:3:" ͂";s:3:"῁";s:5:" ̈͂";s:3:"ῂ";s:6:"ῂ";s:3:"ῃ";s:4:"ῃ";s:3:"ῄ";s:6:"ῄ";s:3:"ῆ";s:4:"ῆ";s:3:"ῇ";s:6:"ῇ";s:3:"Ὲ";s:4:"Ὲ";s:3:"Έ";s:4:"Έ";s:3:"Ὴ";s:4:"Ὴ";s:3:"Ή";s:4:"Ή";s:3:"ῌ";s:4:"ῌ";s:3:"῍";s:5:" ̓̀";s:3:"῎";s:5:" ̓́";s:3:"῏";s:5:" ̓͂";s:3:"ῐ";s:4:"ῐ";s:3:"ῑ";s:4:"ῑ";s:3:"ῒ";s:6:"ῒ";s:3:"ΐ";s:6:"ΐ";s:3:"ῖ";s:4:"ῖ";s:3:"ῗ";s:6:"ῗ";s:3:"Ῐ";s:4:"Ῐ";s:3:"Ῑ";s:4:"Ῑ";s:3:"Ὶ";s:4:"Ὶ";s:3:"Ί";s:4:"Ί";s:3:"῝";s:5:" ̔̀";s:3:"῞";s:5:" ̔́";s:3:"῟";s:5:" ̔͂";s:3:"ῠ";s:4:"ῠ";s:3:"ῡ";s:4:"ῡ";s:3:"ῢ";s:6:"ῢ";s:3:"ΰ";s:6:"ΰ";s:3:"ῤ";s:4:"ῤ";s:3:"ῥ";s:4:"ῥ";s:3:"ῦ";s:4:"ῦ";s:3:"ῧ";s:6:"ῧ";s:3:"Ῠ";s:4:"Ῠ";s:3:"Ῡ";s:4:"Ῡ";s:3:"Ὺ";s:4:"Ὺ";s:3:"Ύ";s:4:"Ύ";s:3:"Ῥ";s:4:"Ῥ";s:3:"῭";s:5:" ̈̀";s:3:"΅";s:5:" ̈́";s:3:"`";s:1:"`";s:3:"ῲ";s:6:"ῲ";s:3:"ῳ";s:4:"ῳ";s:3:"ῴ";s:6:"ῴ";s:3:"ῶ";s:4:"ῶ";s:3:"ῷ";s:6:"ῷ";s:3:"Ὸ";s:4:"Ὸ";s:3:"Ό";s:4:"Ό";s:3:"Ὼ";s:4:"Ὼ";s:3:"Ώ";s:4:"Ώ";s:3:"ῼ";s:4:"ῼ";s:3:"´";s:3:" ́";s:3:"῾";s:3:" ̔";s:3:" ";s:1:" ";s:3:" ";s:1:" ";s:3:" ";s:1:" ";s:3:" ";s:1:" ";s:3:" ";s:1:" ";s:3:" ";s:1:" ";s:3:" ";s:1:" ";s:3:" ";s:1:" ";s:3:" ";s:1:" ";s:3:" ";s:1:" ";s:3:" ";s:1:" ";s:3:"‑";s:3:"‐";s:3:"‗";s:3:" ̳";s:3:"․";s:1:".";s:3:"‥";s:2:"..";s:3:"…";s:3:"...";s:3:" ";s:1:" ";s:3:"″";s:6:"′′";s:3:"‴";s:9:"′′′";s:3:"‶";s:6:"‵‵";s:3:"‷";s:9:"‵‵‵";s:3:"‼";s:2:"!!";s:3:"‾";s:3:" ̅";s:3:"⁇";s:2:"??";s:3:"⁈";s:2:"?!";s:3:"⁉";s:2:"!?";s:3:"⁗";s:12:"′′′′";s:3:" ";s:1:" ";s:3:"⁰";s:1:"0";s:3:"ⁱ";s:1:"i";s:3:"⁴";s:1:"4";s:3:"⁵";s:1:"5";s:3:"⁶";s:1:"6";s:3:"⁷";s:1:"7";s:3:"⁸";s:1:"8";s:3:"⁹";s:1:"9";s:3:"⁺";s:1:"+";s:3:"⁻";s:3:"−";s:3:"⁼";s:1:"=";s:3:"⁽";s:1:"(";s:3:"⁾";s:1:")";s:3:"ⁿ";s:1:"n";s:3:"₀";s:1:"0";s:3:"₁";s:1:"1";s:3:"₂";s:1:"2";s:3:"₃";s:1:"3";s:3:"₄";s:1:"4";s:3:"₅";s:1:"5";s:3:"₆";s:1:"6";s:3:"₇";s:1:"7";s:3:"₈";s:1:"8";s:3:"₉";s:1:"9";s:3:"₊";s:1:"+";s:3:"₋";s:3:"−";s:3:"₌";s:1:"=";s:3:"₍";s:1:"(";s:3:"₎";s:1:")";s:3:"ₐ";s:1:"a";s:3:"ₑ";s:1:"e";s:3:"ₒ";s:1:"o";s:3:"ₓ";s:1:"x";s:3:"ₔ";s:2:"ə";s:3:"₨";s:2:"Rs";s:3:"℀";s:3:"a/c";s:3:"℁";s:3:"a/s";s:3:"ℂ";s:1:"C";s:3:"℃";s:3:"°C";s:3:"℅";s:3:"c/o";s:3:"℆";s:3:"c/u";s:3:"ℇ";s:2:"Ɛ";s:3:"℉";s:3:"°F";s:3:"ℊ";s:1:"g";s:3:"ℋ";s:1:"H";s:3:"ℌ";s:1:"H";s:3:"ℍ";s:1:"H";s:3:"ℎ";s:1:"h";s:3:"ℏ";s:2:"ħ";s:3:"ℐ";s:1:"I";s:3:"ℑ";s:1:"I";s:3:"ℒ";s:1:"L";s:3:"ℓ";s:1:"l";s:3:"ℕ";s:1:"N";s:3:"№";s:2:"No";s:3:"ℙ";s:1:"P";s:3:"ℚ";s:1:"Q";s:3:"ℛ";s:1:"R";s:3:"ℜ";s:1:"R";s:3:"ℝ";s:1:"R";s:3:"℠";s:2:"SM";s:3:"℡";s:3:"TEL";s:3:"™";s:2:"TM";s:3:"ℤ";s:1:"Z";s:3:"Ω";s:2:"Ω";s:3:"ℨ";s:1:"Z";s:3:"K";s:1:"K";s:3:"Å";s:3:"Å";s:3:"ℬ";s:1:"B";s:3:"ℭ";s:1:"C";s:3:"ℯ";s:1:"e";s:3:"ℰ";s:1:"E";s:3:"ℱ";s:1:"F";s:3:"ℳ";s:1:"M";s:3:"ℴ";s:1:"o";s:3:"ℵ";s:2:"א";s:3:"ℶ";s:2:"ב";s:3:"ℷ";s:2:"ג";s:3:"ℸ";s:2:"ד";s:3:"ℹ";s:1:"i";s:3:"℻";s:3:"FAX";s:3:"ℼ";s:2:"π";s:3:"ℽ";s:2:"γ";s:3:"ℾ";s:2:"Γ";s:3:"ℿ";s:2:"Π";s:3:"⅀";s:3:"∑";s:3:"ⅅ";s:1:"D";s:3:"ⅆ";s:1:"d";s:3:"ⅇ";s:1:"e";s:3:"ⅈ";s:1:"i";s:3:"ⅉ";s:1:"j";s:3:"⅓";s:5:"1⁄3";s:3:"⅔";s:5:"2⁄3";s:3:"⅕";s:5:"1⁄5";s:3:"⅖";s:5:"2⁄5";s:3:"⅗";s:5:"3⁄5";s:3:"⅘";s:5:"4⁄5";s:3:"⅙";s:5:"1⁄6";s:3:"⅚";s:5:"5⁄6";s:3:"⅛";s:5:"1⁄8";s:3:"⅜";s:5:"3⁄8";s:3:"⅝";s:5:"5⁄8";s:3:"⅞";s:5:"7⁄8";s:3:"⅟";s:4:"1⁄";s:3:"Ⅰ";s:1:"I";s:3:"Ⅱ";s:2:"II";s:3:"Ⅲ";s:3:"III";s:3:"Ⅳ";s:2:"IV";s:3:"Ⅴ";s:1:"V";s:3:"Ⅵ";s:2:"VI";s:3:"Ⅶ";s:3:"VII";s:3:"Ⅷ";s:4:"VIII";s:3:"Ⅸ";s:2:"IX";s:3:"Ⅹ";s:1:"X";s:3:"Ⅺ";s:2:"XI";s:3:"Ⅻ";s:3:"XII";s:3:"Ⅼ";s:1:"L";s:3:"Ⅽ";s:1:"C";s:3:"Ⅾ";s:1:"D";s:3:"Ⅿ";s:1:"M";s:3:"ⅰ";s:1:"i";s:3:"ⅱ";s:2:"ii";s:3:"ⅲ";s:3:"iii";s:3:"ⅳ";s:2:"iv";s:3:"ⅴ";s:1:"v";s:3:"ⅵ";s:2:"vi";s:3:"ⅶ";s:3:"vii";s:3:"ⅷ";s:4:"viii";s:3:"ⅸ";s:2:"ix";s:3:"ⅹ";s:1:"x";s:3:"ⅺ";s:2:"xi";s:3:"ⅻ";s:3:"xii";s:3:"ⅼ";s:1:"l";s:3:"ⅽ";s:1:"c";s:3:"ⅾ";s:1:"d";s:3:"ⅿ";s:1:"m";s:3:"↚";s:5:"↚";s:3:"↛";s:5:"↛";s:3:"↮";s:5:"↮";s:3:"⇍";s:5:"⇍";s:3:"⇎";s:5:"⇎";s:3:"⇏";s:5:"⇏";s:3:"∄";s:5:"∄";s:3:"∉";s:5:"∉";s:3:"∌";s:5:"∌";s:3:"∤";s:5:"∤";s:3:"∦";s:5:"∦";s:3:"∬";s:6:"∫∫";s:3:"∭";s:9:"∫∫∫";s:3:"∯";s:6:"∮∮";s:3:"∰";s:9:"∮∮∮";s:3:"≁";s:5:"≁";s:3:"≄";s:5:"≄";s:3:"≇";s:5:"≇";s:3:"≉";s:5:"≉";s:3:"≠";s:3:"≠";s:3:"≢";s:5:"≢";s:3:"≭";s:5:"≭";s:3:"≮";s:3:"≮";s:3:"≯";s:3:"≯";s:3:"≰";s:5:"≰";s:3:"≱";s:5:"≱";s:3:"≴";s:5:"≴";s:3:"≵";s:5:"≵";s:3:"≸";s:5:"≸";s:3:"≹";s:5:"≹";s:3:"⊀";s:5:"⊀";s:3:"⊁";s:5:"⊁";s:3:"⊄";s:5:"⊄";s:3:"⊅";s:5:"⊅";s:3:"⊈";s:5:"⊈";s:3:"⊉";s:5:"⊉";s:3:"⊬";s:5:"⊬";s:3:"⊭";s:5:"⊭";s:3:"⊮";s:5:"⊮";s:3:"⊯";s:5:"⊯";s:3:"⋠";s:5:"⋠";s:3:"⋡";s:5:"⋡";s:3:"⋢";s:5:"⋢";s:3:"⋣";s:5:"⋣";s:3:"⋪";s:5:"⋪";s:3:"⋫";s:5:"⋫";s:3:"⋬";s:5:"⋬";s:3:"⋭";s:5:"⋭";s:3:"〈";s:3:"〈";s:3:"〉";s:3:"〉";s:3:"①";s:1:"1";s:3:"②";s:1:"2";s:3:"③";s:1:"3";s:3:"④";s:1:"4";s:3:"⑤";s:1:"5";s:3:"⑥";s:1:"6";s:3:"⑦";s:1:"7";s:3:"⑧";s:1:"8";s:3:"⑨";s:1:"9";s:3:"⑩";s:2:"10";s:3:"⑪";s:2:"11";s:3:"⑫";s:2:"12";s:3:"⑬";s:2:"13";s:3:"⑭";s:2:"14";s:3:"⑮";s:2:"15";s:3:"⑯";s:2:"16";s:3:"⑰";s:2:"17";s:3:"⑱";s:2:"18";s:3:"⑲";s:2:"19";s:3:"⑳";s:2:"20";s:3:"⑴";s:3:"(1)";s:3:"⑵";s:3:"(2)";s:3:"⑶";s:3:"(3)";s:3:"⑷";s:3:"(4)";s:3:"⑸";s:3:"(5)";s:3:"⑹";s:3:"(6)";s:3:"⑺";s:3:"(7)";s:3:"⑻";s:3:"(8)";s:3:"⑼";s:3:"(9)";s:3:"⑽";s:4:"(10)";s:3:"⑾";s:4:"(11)";s:3:"⑿";s:4:"(12)";s:3:"⒀";s:4:"(13)";s:3:"⒁";s:4:"(14)";s:3:"⒂";s:4:"(15)";s:3:"⒃";s:4:"(16)";s:3:"⒄";s:4:"(17)";s:3:"⒅";s:4:"(18)";s:3:"⒆";s:4:"(19)";s:3:"⒇";s:4:"(20)";s:3:"⒈";s:2:"1.";s:3:"⒉";s:2:"2.";s:3:"⒊";s:2:"3.";s:3:"⒋";s:2:"4.";s:3:"⒌";s:2:"5.";s:3:"⒍";s:2:"6.";s:3:"⒎";s:2:"7.";s:3:"⒏";s:2:"8.";s:3:"⒐";s:2:"9.";s:3:"⒑";s:3:"10.";s:3:"⒒";s:3:"11.";s:3:"⒓";s:3:"12.";s:3:"⒔";s:3:"13.";s:3:"⒕";s:3:"14.";s:3:"⒖";s:3:"15.";s:3:"⒗";s:3:"16.";s:3:"⒘";s:3:"17.";s:3:"⒙";s:3:"18.";s:3:"⒚";s:3:"19.";s:3:"⒛";s:3:"20.";s:3:"⒜";s:3:"(a)";s:3:"⒝";s:3:"(b)";s:3:"⒞";s:3:"(c)";s:3:"⒟";s:3:"(d)";s:3:"⒠";s:3:"(e)";s:3:"⒡";s:3:"(f)";s:3:"⒢";s:3:"(g)";s:3:"⒣";s:3:"(h)";s:3:"⒤";s:3:"(i)";s:3:"⒥";s:3:"(j)";s:3:"⒦";s:3:"(k)";s:3:"⒧";s:3:"(l)";s:3:"⒨";s:3:"(m)";s:3:"⒩";s:3:"(n)";s:3:"⒪";s:3:"(o)";s:3:"⒫";s:3:"(p)";s:3:"⒬";s:3:"(q)";s:3:"⒭";s:3:"(r)";s:3:"⒮";s:3:"(s)";s:3:"⒯";s:3:"(t)";s:3:"⒰";s:3:"(u)";s:3:"⒱";s:3:"(v)";s:3:"⒲";s:3:"(w)";s:3:"⒳";s:3:"(x)";s:3:"⒴";s:3:"(y)";s:3:"⒵";s:3:"(z)";s:3:"Ⓐ";s:1:"A";s:3:"Ⓑ";s:1:"B";s:3:"Ⓒ";s:1:"C";s:3:"Ⓓ";s:1:"D";s:3:"Ⓔ";s:1:"E";s:3:"Ⓕ";s:1:"F";s:3:"Ⓖ";s:1:"G";s:3:"Ⓗ";s:1:"H";s:3:"Ⓘ";s:1:"I";s:3:"Ⓙ";s:1:"J";s:3:"Ⓚ";s:1:"K";s:3:"Ⓛ";s:1:"L";s:3:"Ⓜ";s:1:"M";s:3:"Ⓝ";s:1:"N";s:3:"Ⓞ";s:1:"O";s:3:"Ⓟ";s:1:"P";s:3:"Ⓠ";s:1:"Q";s:3:"Ⓡ";s:1:"R";s:3:"Ⓢ";s:1:"S";s:3:"Ⓣ";s:1:"T";s:3:"Ⓤ";s:1:"U";s:3:"Ⓥ";s:1:"V";s:3:"Ⓦ";s:1:"W";s:3:"Ⓧ";s:1:"X";s:3:"Ⓨ";s:1:"Y";s:3:"Ⓩ";s:1:"Z";s:3:"ⓐ";s:1:"a";s:3:"ⓑ";s:1:"b";s:3:"ⓒ";s:1:"c";s:3:"ⓓ";s:1:"d";s:3:"ⓔ";s:1:"e";s:3:"ⓕ";s:1:"f";s:3:"ⓖ";s:1:"g";s:3:"ⓗ";s:1:"h";s:3:"ⓘ";s:1:"i";s:3:"ⓙ";s:1:"j";s:3:"ⓚ";s:1:"k";s:3:"ⓛ";s:1:"l";s:3:"ⓜ";s:1:"m";s:3:"ⓝ";s:1:"n";s:3:"ⓞ";s:1:"o";s:3:"ⓟ";s:1:"p";s:3:"ⓠ";s:1:"q";s:3:"ⓡ";s:1:"r";s:3:"ⓢ";s:1:"s";s:3:"ⓣ";s:1:"t";s:3:"ⓤ";s:1:"u";s:3:"ⓥ";s:1:"v";s:3:"ⓦ";s:1:"w";s:3:"ⓧ";s:1:"x";s:3:"ⓨ";s:1:"y";s:3:"ⓩ";s:1:"z";s:3:"⓪";s:1:"0";s:3:"⨌";s:12:"∫∫∫∫";s:3:"⩴";s:3:"::=";s:3:"⩵";s:2:"==";s:3:"⩶";s:3:"===";s:3:"⫝̸";s:5:"⫝̸";s:3:"ⵯ";s:3:"ⵡ";s:3:"⺟";s:3:"母";s:3:"⻳";s:3:"龟";s:3:"⼀";s:3:"一";s:3:"⼁";s:3:"丨";s:3:"⼂";s:3:"丶";s:3:"⼃";s:3:"丿";s:3:"⼄";s:3:"乙";s:3:"⼅";s:3:"亅";s:3:"⼆";s:3:"二";s:3:"⼇";s:3:"亠";s:3:"⼈";s:3:"人";s:3:"⼉";s:3:"儿";s:3:"⼊";s:3:"入";s:3:"⼋";s:3:"八";s:3:"⼌";s:3:"冂";s:3:"⼍";s:3:"冖";s:3:"⼎";s:3:"冫";s:3:"⼏";s:3:"几";s:3:"⼐";s:3:"凵";s:3:"⼑";s:3:"刀";s:3:"⼒";s:3:"力";s:3:"⼓";s:3:"勹";s:3:"⼔";s:3:"匕";s:3:"⼕";s:3:"匚";s:3:"⼖";s:3:"匸";s:3:"⼗";s:3:"十";s:3:"⼘";s:3:"卜";s:3:"⼙";s:3:"卩";s:3:"⼚";s:3:"厂";s:3:"⼛";s:3:"厶";s:3:"⼜";s:3:"又";s:3:"⼝";s:3:"口";s:3:"⼞";s:3:"囗";s:3:"⼟";s:3:"土";s:3:"⼠";s:3:"士";s:3:"⼡";s:3:"夂";s:3:"⼢";s:3:"夊";s:3:"⼣";s:3:"夕";s:3:"⼤";s:3:"大";s:3:"⼥";s:3:"女";s:3:"⼦";s:3:"子";s:3:"⼧";s:3:"宀";s:3:"⼨";s:3:"寸";s:3:"⼩";s:3:"小";s:3:"⼪";s:3:"尢";s:3:"⼫";s:3:"尸";s:3:"⼬";s:3:"屮";s:3:"⼭";s:3:"山";s:3:"⼮";s:3:"巛";s:3:"⼯";s:3:"工";s:3:"⼰";s:3:"己";s:3:"⼱";s:3:"巾";s:3:"⼲";s:3:"干";s:3:"⼳";s:3:"幺";s:3:"⼴";s:3:"广";s:3:"⼵";s:3:"廴";s:3:"⼶";s:3:"廾";s:3:"⼷";s:3:"弋";s:3:"⼸";s:3:"弓";s:3:"⼹";s:3:"彐";s:3:"⼺";s:3:"彡";s:3:"⼻";s:3:"彳";s:3:"⼼";s:3:"心";s:3:"⼽";s:3:"戈";s:3:"⼾";s:3:"戶";s:3:"⼿";s:3:"手";s:3:"⽀";s:3:"支";s:3:"⽁";s:3:"攴";s:3:"⽂";s:3:"文";s:3:"⽃";s:3:"斗";s:3:"⽄";s:3:"斤";s:3:"⽅";s:3:"方";s:3:"⽆";s:3:"无";s:3:"⽇";s:3:"日";s:3:"⽈";s:3:"曰";s:3:"⽉";s:3:"月";s:3:"⽊";s:3:"木";s:3:"⽋";s:3:"欠";s:3:"⽌";s:3:"止";s:3:"⽍";s:3:"歹";s:3:"⽎";s:3:"殳";s:3:"⽏";s:3:"毋";s:3:"⽐";s:3:"比";s:3:"⽑";s:3:"毛";s:3:"⽒";s:3:"氏";s:3:"⽓";s:3:"气";s:3:"⽔";s:3:"水";s:3:"⽕";s:3:"火";s:3:"⽖";s:3:"爪";s:3:"⽗";s:3:"父";s:3:"⽘";s:3:"爻";s:3:"⽙";s:3:"爿";s:3:"⽚";s:3:"片";s:3:"⽛";s:3:"牙";s:3:"⽜";s:3:"牛";s:3:"⽝";s:3:"犬";s:3:"⽞";s:3:"玄";s:3:"⽟";s:3:"玉";s:3:"⽠";s:3:"瓜";s:3:"⽡";s:3:"瓦";s:3:"⽢";s:3:"甘";s:3:"⽣";s:3:"生";s:3:"⽤";s:3:"用";s:3:"⽥";s:3:"田";s:3:"⽦";s:3:"疋";s:3:"⽧";s:3:"疒";s:3:"⽨";s:3:"癶";s:3:"⽩";s:3:"白";s:3:"⽪";s:3:"皮";s:3:"⽫";s:3:"皿";s:3:"⽬";s:3:"目";s:3:"⽭";s:3:"矛";s:3:"⽮";s:3:"矢";s:3:"⽯";s:3:"石";s:3:"⽰";s:3:"示";s:3:"⽱";s:3:"禸";s:3:"⽲";s:3:"禾";s:3:"⽳";s:3:"穴";s:3:"⽴";s:3:"立";s:3:"⽵";s:3:"竹";s:3:"⽶";s:3:"米";s:3:"⽷";s:3:"糸";s:3:"⽸";s:3:"缶";s:3:"⽹";s:3:"网";s:3:"⽺";s:3:"羊";s:3:"⽻";s:3:"羽";s:3:"⽼";s:3:"老";s:3:"⽽";s:3:"而";s:3:"⽾";s:3:"耒";s:3:"⽿";s:3:"耳";s:3:"⾀";s:3:"聿";s:3:"⾁";s:3:"肉";s:3:"⾂";s:3:"臣";s:3:"⾃";s:3:"自";s:3:"⾄";s:3:"至";s:3:"⾅";s:3:"臼";s:3:"⾆";s:3:"舌";s:3:"⾇";s:3:"舛";s:3:"⾈";s:3:"舟";s:3:"⾉";s:3:"艮";s:3:"⾊";s:3:"色";s:3:"⾋";s:3:"艸";s:3:"⾌";s:3:"虍";s:3:"⾍";s:3:"虫";s:3:"⾎";s:3:"血";s:3:"⾏";s:3:"行";s:3:"⾐";s:3:"衣";s:3:"⾑";s:3:"襾";s:3:"⾒";s:3:"見";s:3:"⾓";s:3:"角";s:3:"⾔";s:3:"言";s:3:"⾕";s:3:"谷";s:3:"⾖";s:3:"豆";s:3:"⾗";s:3:"豕";s:3:"⾘";s:3:"豸";s:3:"⾙";s:3:"貝";s:3:"⾚";s:3:"赤";s:3:"⾛";s:3:"走";s:3:"⾜";s:3:"足";s:3:"⾝";s:3:"身";s:3:"⾞";s:3:"車";s:3:"⾟";s:3:"辛";s:3:"⾠";s:3:"辰";s:3:"⾡";s:3:"辵";s:3:"⾢";s:3:"邑";s:3:"⾣";s:3:"酉";s:3:"⾤";s:3:"釆";s:3:"⾥";s:3:"里";s:3:"⾦";s:3:"金";s:3:"⾧";s:3:"長";s:3:"⾨";s:3:"門";s:3:"⾩";s:3:"阜";s:3:"⾪";s:3:"隶";s:3:"⾫";s:3:"隹";s:3:"⾬";s:3:"雨";s:3:"⾭";s:3:"靑";s:3:"⾮";s:3:"非";s:3:"⾯";s:3:"面";s:3:"⾰";s:3:"革";s:3:"⾱";s:3:"韋";s:3:"⾲";s:3:"韭";s:3:"⾳";s:3:"音";s:3:"⾴";s:3:"頁";s:3:"⾵";s:3:"風";s:3:"⾶";s:3:"飛";s:3:"⾷";s:3:"食";s:3:"⾸";s:3:"首";s:3:"⾹";s:3:"香";s:3:"⾺";s:3:"馬";s:3:"⾻";s:3:"骨";s:3:"⾼";s:3:"高";s:3:"⾽";s:3:"髟";s:3:"⾾";s:3:"鬥";s:3:"⾿";s:3:"鬯";s:3:"⿀";s:3:"鬲";s:3:"⿁";s:3:"鬼";s:3:"⿂";s:3:"魚";s:3:"⿃";s:3:"鳥";s:3:"⿄";s:3:"鹵";s:3:"⿅";s:3:"鹿";s:3:"⿆";s:3:"麥";s:3:"⿇";s:3:"麻";s:3:"⿈";s:3:"黃";s:3:"⿉";s:3:"黍";s:3:"⿊";s:3:"黑";s:3:"⿋";s:3:"黹";s:3:"⿌";s:3:"黽";s:3:"⿍";s:3:"鼎";s:3:"⿎";s:3:"鼓";s:3:"⿏";s:3:"鼠";s:3:"⿐";s:3:"鼻";s:3:"⿑";s:3:"齊";s:3:"⿒";s:3:"齒";s:3:"⿓";s:3:"龍";s:3:"⿔";s:3:"龜";s:3:"⿕";s:3:"龠";s:3:" ";s:1:" ";s:3:"〶";s:3:"〒";s:3:"〸";s:3:"十";s:3:"〹";s:3:"卄";s:3:"〺";s:3:"卅";s:3:"が";s:6:"が";s:3:"ぎ";s:6:"ぎ";s:3:"ぐ";s:6:"ぐ";s:3:"げ";s:6:"げ";s:3:"ご";s:6:"ご";s:3:"ざ";s:6:"ざ";s:3:"じ";s:6:"じ";s:3:"ず";s:6:"ず";s:3:"ぜ";s:6:"ぜ";s:3:"ぞ";s:6:"ぞ";s:3:"だ";s:6:"だ";s:3:"ぢ";s:6:"ぢ";s:3:"づ";s:6:"づ";s:3:"で";s:6:"で";s:3:"ど";s:6:"ど";s:3:"ば";s:6:"ば";s:3:"ぱ";s:6:"ぱ";s:3:"び";s:6:"び";s:3:"ぴ";s:6:"ぴ";s:3:"ぶ";s:6:"ぶ";s:3:"ぷ";s:6:"ぷ";s:3:"べ";s:6:"べ";s:3:"ぺ";s:6:"ぺ";s:3:"ぼ";s:6:"ぼ";s:3:"ぽ";s:6:"ぽ";s:3:"ゔ";s:6:"ゔ";s:3:"゛";s:4:" ゙";s:3:"゜";s:4:" ゚";s:3:"ゞ";s:6:"ゞ";s:3:"ゟ";s:6:"より";s:3:"ガ";s:6:"ガ";s:3:"ギ";s:6:"ギ";s:3:"グ";s:6:"グ";s:3:"ゲ";s:6:"ゲ";s:3:"ゴ";s:6:"ゴ";s:3:"ザ";s:6:"ザ";s:3:"ジ";s:6:"ジ";s:3:"ズ";s:6:"ズ";s:3:"ゼ";s:6:"ゼ";s:3:"ゾ";s:6:"ゾ";s:3:"ダ";s:6:"ダ";s:3:"ヂ";s:6:"ヂ";s:3:"ヅ";s:6:"ヅ";s:3:"デ";s:6:"デ";s:3:"ド";s:6:"ド";s:3:"バ";s:6:"バ";s:3:"パ";s:6:"パ";s:3:"ビ";s:6:"ビ";s:3:"ピ";s:6:"ピ";s:3:"ブ";s:6:"ブ";s:3:"プ";s:6:"プ";s:3:"ベ";s:6:"ベ";s:3:"ペ";s:6:"ペ";s:3:"ボ";s:6:"ボ";s:3:"ポ";s:6:"ポ";s:3:"ヴ";s:6:"ヴ";s:3:"ヷ";s:6:"ヷ";s:3:"ヸ";s:6:"ヸ";s:3:"ヹ";s:6:"ヹ";s:3:"ヺ";s:6:"ヺ";s:3:"ヾ";s:6:"ヾ";s:3:"ヿ";s:6:"コト";s:3:"ㄱ";s:3:"ᄀ";s:3:"ㄲ";s:3:"ᄁ";s:3:"ㄳ";s:3:"ᆪ";s:3:"ㄴ";s:3:"ᄂ";s:3:"ㄵ";s:3:"ᆬ";s:3:"ㄶ";s:3:"ᆭ";s:3:"ㄷ";s:3:"ᄃ";s:3:"ㄸ";s:3:"ᄄ";s:3:"ㄹ";s:3:"ᄅ";s:3:"ㄺ";s:3:"ᆰ";s:3:"ㄻ";s:3:"ᆱ";s:3:"ㄼ";s:3:"ᆲ";s:3:"ㄽ";s:3:"ᆳ";s:3:"ㄾ";s:3:"ᆴ";s:3:"ㄿ";s:3:"ᆵ";s:3:"ㅀ";s:3:"ᄚ";s:3:"ㅁ";s:3:"ᄆ";s:3:"ㅂ";s:3:"ᄇ";s:3:"ㅃ";s:3:"ᄈ";s:3:"ㅄ";s:3:"ᄡ";s:3:"ㅅ";s:3:"ᄉ";s:3:"ㅆ";s:3:"ᄊ";s:3:"ㅇ";s:3:"ᄋ";s:3:"ㅈ";s:3:"ᄌ";s:3:"ㅉ";s:3:"ᄍ";s:3:"ㅊ";s:3:"ᄎ";s:3:"ㅋ";s:3:"ᄏ";s:3:"ㅌ";s:3:"ᄐ";s:3:"ㅍ";s:3:"ᄑ";s:3:"ㅎ";s:3:"ᄒ";s:3:"ㅏ";s:3:"ᅡ";s:3:"ㅐ";s:3:"ᅢ";s:3:"ㅑ";s:3:"ᅣ";s:3:"ㅒ";s:3:"ᅤ";s:3:"ㅓ";s:3:"ᅥ";s:3:"ㅔ";s:3:"ᅦ";s:3:"ㅕ";s:3:"ᅧ";s:3:"ㅖ";s:3:"ᅨ";s:3:"ㅗ";s:3:"ᅩ";s:3:"ㅘ";s:3:"ᅪ";s:3:"ㅙ";s:3:"ᅫ";s:3:"ㅚ";s:3:"ᅬ";s:3:"ㅛ";s:3:"ᅭ";s:3:"ㅜ";s:3:"ᅮ";s:3:"ㅝ";s:3:"ᅯ";s:3:"ㅞ";s:3:"ᅰ";s:3:"ㅟ";s:3:"ᅱ";s:3:"ㅠ";s:3:"ᅲ";s:3:"ㅡ";s:3:"ᅳ";s:3:"ㅢ";s:3:"ᅴ";s:3:"ㅣ";s:3:"ᅵ";s:3:"ㅤ";s:3:"ᅠ";s:3:"ㅥ";s:3:"ᄔ";s:3:"ㅦ";s:3:"ᄕ";s:3:"ㅧ";s:3:"ᇇ";s:3:"ㅨ";s:3:"ᇈ";s:3:"ㅩ";s:3:"ᇌ";s:3:"ㅪ";s:3:"ᇎ";s:3:"ㅫ";s:3:"ᇓ";s:3:"ㅬ";s:3:"ᇗ";s:3:"ㅭ";s:3:"ᇙ";s:3:"ㅮ";s:3:"ᄜ";s:3:"ㅯ";s:3:"ᇝ";s:3:"ㅰ";s:3:"ᇟ";s:3:"ㅱ";s:3:"ᄝ";s:3:"ㅲ";s:3:"ᄞ";s:3:"ㅳ";s:3:"ᄠ";s:3:"ㅴ";s:3:"ᄢ";s:3:"ㅵ";s:3:"ᄣ";s:3:"ㅶ";s:3:"ᄧ";s:3:"ㅷ";s:3:"ᄩ";s:3:"ㅸ";s:3:"ᄫ";s:3:"ㅹ";s:3:"ᄬ";s:3:"ㅺ";s:3:"ᄭ";s:3:"ㅻ";s:3:"ᄮ";s:3:"ㅼ";s:3:"ᄯ";s:3:"ㅽ";s:3:"ᄲ";s:3:"ㅾ";s:3:"ᄶ";s:3:"ㅿ";s:3:"ᅀ";s:3:"ㆀ";s:3:"ᅇ";s:3:"ㆁ";s:3:"ᅌ";s:3:"ㆂ";s:3:"ᇱ";s:3:"ㆃ";s:3:"ᇲ";s:3:"ㆄ";s:3:"ᅗ";s:3:"ㆅ";s:3:"ᅘ";s:3:"ㆆ";s:3:"ᅙ";s:3:"ㆇ";s:3:"ᆄ";s:3:"ㆈ";s:3:"ᆅ";s:3:"ㆉ";s:3:"ᆈ";s:3:"ㆊ";s:3:"ᆑ";s:3:"ㆋ";s:3:"ᆒ";s:3:"ㆌ";s:3:"ᆔ";s:3:"ㆍ";s:3:"ᆞ";s:3:"ㆎ";s:3:"ᆡ";s:3:"㆒";s:3:"一";s:3:"㆓";s:3:"二";s:3:"㆔";s:3:"三";s:3:"㆕";s:3:"四";s:3:"㆖";s:3:"上";s:3:"㆗";s:3:"中";s:3:"㆘";s:3:"下";s:3:"㆙";s:3:"甲";s:3:"㆚";s:3:"乙";s:3:"㆛";s:3:"丙";s:3:"㆜";s:3:"丁";s:3:"㆝";s:3:"天";s:3:"㆞";s:3:"地";s:3:"㆟";s:3:"人";s:3:"㈀";s:5:"(ᄀ)";s:3:"㈁";s:5:"(ᄂ)";s:3:"㈂";s:5:"(ᄃ)";s:3:"㈃";s:5:"(ᄅ)";s:3:"㈄";s:5:"(ᄆ)";s:3:"㈅";s:5:"(ᄇ)";s:3:"㈆";s:5:"(ᄉ)";s:3:"㈇";s:5:"(ᄋ)";s:3:"㈈";s:5:"(ᄌ)";s:3:"㈉";s:5:"(ᄎ)";s:3:"㈊";s:5:"(ᄏ)";s:3:"㈋";s:5:"(ᄐ)";s:3:"㈌";s:5:"(ᄑ)";s:3:"㈍";s:5:"(ᄒ)";s:3:"㈎";s:8:"(가)";s:3:"㈏";s:8:"(나)";s:3:"㈐";s:8:"(다)";s:3:"㈑";s:8:"(라)";s:3:"㈒";s:8:"(마)";s:3:"㈓";s:8:"(바)";s:3:"㈔";s:8:"(사)";s:3:"㈕";s:8:"(아)";s:3:"㈖";s:8:"(자)";s:3:"㈗";s:8:"(차)";s:3:"㈘";s:8:"(카)";s:3:"㈙";s:8:"(타)";s:3:"㈚";s:8:"(파)";s:3:"㈛";s:8:"(하)";s:3:"㈜";s:8:"(주)";s:3:"㈝";s:17:"(오전)";s:3:"㈞";s:14:"(오후)";s:3:"㈠";s:5:"(一)";s:3:"㈡";s:5:"(二)";s:3:"㈢";s:5:"(三)";s:3:"㈣";s:5:"(四)";s:3:"㈤";s:5:"(五)";s:3:"㈥";s:5:"(六)";s:3:"㈦";s:5:"(七)";s:3:"㈧";s:5:"(八)";s:3:"㈨";s:5:"(九)";s:3:"㈩";s:5:"(十)";s:3:"㈪";s:5:"(月)";s:3:"㈫";s:5:"(火)";s:3:"㈬";s:5:"(水)";s:3:"㈭";s:5:"(木)";s:3:"㈮";s:5:"(金)";s:3:"㈯";s:5:"(土)";s:3:"㈰";s:5:"(日)";s:3:"㈱";s:5:"(株)";s:3:"㈲";s:5:"(有)";s:3:"㈳";s:5:"(社)";s:3:"㈴";s:5:"(名)";s:3:"㈵";s:5:"(特)";s:3:"㈶";s:5:"(財)";s:3:"㈷";s:5:"(祝)";s:3:"㈸";s:5:"(労)";s:3:"㈹";s:5:"(代)";s:3:"㈺";s:5:"(呼)";s:3:"㈻";s:5:"(学)";s:3:"㈼";s:5:"(監)";s:3:"㈽";s:5:"(企)";s:3:"㈾";s:5:"(資)";s:3:"㈿";s:5:"(協)";s:3:"㉀";s:5:"(祭)";s:3:"㉁";s:5:"(休)";s:3:"㉂";s:5:"(自)";s:3:"㉃";s:5:"(至)";s:3:"㉐";s:3:"PTE";s:3:"㉑";s:2:"21";s:3:"㉒";s:2:"22";s:3:"㉓";s:2:"23";s:3:"㉔";s:2:"24";s:3:"㉕";s:2:"25";s:3:"㉖";s:2:"26";s:3:"㉗";s:2:"27";s:3:"㉘";s:2:"28";s:3:"㉙";s:2:"29";s:3:"㉚";s:2:"30";s:3:"㉛";s:2:"31";s:3:"㉜";s:2:"32";s:3:"㉝";s:2:"33";s:3:"㉞";s:2:"34";s:3:"㉟";s:2:"35";s:3:"㉠";s:3:"ᄀ";s:3:"㉡";s:3:"ᄂ";s:3:"㉢";s:3:"ᄃ";s:3:"㉣";s:3:"ᄅ";s:3:"㉤";s:3:"ᄆ";s:3:"㉥";s:3:"ᄇ";s:3:"㉦";s:3:"ᄉ";s:3:"㉧";s:3:"ᄋ";s:3:"㉨";s:3:"ᄌ";s:3:"㉩";s:3:"ᄎ";s:3:"㉪";s:3:"ᄏ";s:3:"㉫";s:3:"ᄐ";s:3:"㉬";s:3:"ᄑ";s:3:"㉭";s:3:"ᄒ";s:3:"㉮";s:6:"가";s:3:"㉯";s:6:"나";s:3:"㉰";s:6:"다";s:3:"㉱";s:6:"라";s:3:"㉲";s:6:"마";s:3:"㉳";s:6:"바";s:3:"㉴";s:6:"사";s:3:"㉵";s:6:"아";s:3:"㉶";s:6:"자";s:3:"㉷";s:6:"차";s:3:"㉸";s:6:"카";s:3:"㉹";s:6:"타";s:3:"㉺";s:6:"파";s:3:"㉻";s:6:"하";s:3:"㉼";s:15:"참고";s:3:"㉽";s:12:"주의";s:3:"㉾";s:6:"우";s:3:"㊀";s:3:"一";s:3:"㊁";s:3:"二";s:3:"㊂";s:3:"三";s:3:"㊃";s:3:"四";s:3:"㊄";s:3:"五";s:3:"㊅";s:3:"六";s:3:"㊆";s:3:"七";s:3:"㊇";s:3:"八";s:3:"㊈";s:3:"九";s:3:"㊉";s:3:"十";s:3:"㊊";s:3:"月";s:3:"㊋";s:3:"火";s:3:"㊌";s:3:"水";s:3:"㊍";s:3:"木";s:3:"㊎";s:3:"金";s:3:"㊏";s:3:"土";s:3:"㊐";s:3:"日";s:3:"㊑";s:3:"株";s:3:"㊒";s:3:"有";s:3:"㊓";s:3:"社";s:3:"㊔";s:3:"名";s:3:"㊕";s:3:"特";s:3:"㊖";s:3:"財";s:3:"㊗";s:3:"祝";s:3:"㊘";s:3:"労";s:3:"㊙";s:3:"秘";s:3:"㊚";s:3:"男";s:3:"㊛";s:3:"女";s:3:"㊜";s:3:"適";s:3:"㊝";s:3:"優";s:3:"㊞";s:3:"印";s:3:"㊟";s:3:"注";s:3:"㊠";s:3:"項";s:3:"㊡";s:3:"休";s:3:"㊢";s:3:"写";s:3:"㊣";s:3:"正";s:3:"㊤";s:3:"上";s:3:"㊥";s:3:"中";s:3:"㊦";s:3:"下";s:3:"㊧";s:3:"左";s:3:"㊨";s:3:"右";s:3:"㊩";s:3:"医";s:3:"㊪";s:3:"宗";s:3:"㊫";s:3:"学";s:3:"㊬";s:3:"監";s:3:"㊭";s:3:"企";s:3:"㊮";s:3:"資";s:3:"㊯";s:3:"協";s:3:"㊰";s:3:"夜";s:3:"㊱";s:2:"36";s:3:"㊲";s:2:"37";s:3:"㊳";s:2:"38";s:3:"㊴";s:2:"39";s:3:"㊵";s:2:"40";s:3:"㊶";s:2:"41";s:3:"㊷";s:2:"42";s:3:"㊸";s:2:"43";s:3:"㊹";s:2:"44";s:3:"㊺";s:2:"45";s:3:"㊻";s:2:"46";s:3:"㊼";s:2:"47";s:3:"㊽";s:2:"48";s:3:"㊾";s:2:"49";s:3:"㊿";s:2:"50";s:3:"㋀";s:4:"1月";s:3:"㋁";s:4:"2月";s:3:"㋂";s:4:"3月";s:3:"㋃";s:4:"4月";s:3:"㋄";s:4:"5月";s:3:"㋅";s:4:"6月";s:3:"㋆";s:4:"7月";s:3:"㋇";s:4:"8月";s:3:"㋈";s:4:"9月";s:3:"㋉";s:5:"10月";s:3:"㋊";s:5:"11月";s:3:"㋋";s:5:"12月";s:3:"㋌";s:2:"Hg";s:3:"㋍";s:3:"erg";s:3:"㋎";s:2:"eV";s:3:"㋏";s:3:"LTD";s:3:"㋐";s:3:"ア";s:3:"㋑";s:3:"イ";s:3:"㋒";s:3:"ウ";s:3:"㋓";s:3:"エ";s:3:"㋔";s:3:"オ";s:3:"㋕";s:3:"カ";s:3:"㋖";s:3:"キ";s:3:"㋗";s:3:"ク";s:3:"㋘";s:3:"ケ";s:3:"㋙";s:3:"コ";s:3:"㋚";s:3:"サ";s:3:"㋛";s:3:"シ";s:3:"㋜";s:3:"ス";s:3:"㋝";s:3:"セ";s:3:"㋞";s:3:"ソ";s:3:"㋟";s:3:"タ";s:3:"㋠";s:3:"チ";s:3:"㋡";s:3:"ツ";s:3:"㋢";s:3:"テ";s:3:"㋣";s:3:"ト";s:3:"㋤";s:3:"ナ";s:3:"㋥";s:3:"ニ";s:3:"㋦";s:3:"ヌ";s:3:"㋧";s:3:"ネ";s:3:"㋨";s:3:"ノ";s:3:"㋩";s:3:"ハ";s:3:"㋪";s:3:"ヒ";s:3:"㋫";s:3:"フ";s:3:"㋬";s:3:"ヘ";s:3:"㋭";s:3:"ホ";s:3:"㋮";s:3:"マ";s:3:"㋯";s:3:"ミ";s:3:"㋰";s:3:"ム";s:3:"㋱";s:3:"メ";s:3:"㋲";s:3:"モ";s:3:"㋳";s:3:"ヤ";s:3:"㋴";s:3:"ユ";s:3:"㋵";s:3:"ヨ";s:3:"㋶";s:3:"ラ";s:3:"㋷";s:3:"リ";s:3:"㋸";s:3:"ル";s:3:"㋹";s:3:"レ";s:3:"㋺";s:3:"ロ";s:3:"㋻";s:3:"ワ";s:3:"㋼";s:3:"ヰ";s:3:"㋽";s:3:"ヱ";s:3:"㋾";s:3:"ヲ";s:3:"㌀";s:15:"アパート";s:3:"㌁";s:12:"アルファ";s:3:"㌂";s:15:"アンペア";s:3:"㌃";s:9:"アール";s:3:"㌄";s:15:"イニング";s:3:"㌅";s:9:"インチ";s:3:"㌆";s:9:"ウォン";s:3:"㌇";s:18:"エスクード";s:3:"㌈";s:12:"エーカー";s:3:"㌉";s:9:"オンス";s:3:"㌊";s:9:"オーム";s:3:"㌋";s:9:"カイリ";s:3:"㌌";s:12:"カラット";s:3:"㌍";s:12:"カロリー";s:3:"㌎";s:12:"ガロン";s:3:"㌏";s:12:"ガンマ";s:3:"㌐";s:12:"ギガ";s:3:"㌑";s:12:"ギニー";s:3:"㌒";s:12:"キュリー";s:3:"㌓";s:18:"ギルダー";s:3:"㌔";s:6:"キロ";s:3:"㌕";s:18:"キログラム";s:3:"㌖";s:18:"キロメートル";s:3:"㌗";s:15:"キロワット";s:3:"㌘";s:12:"グラム";s:3:"㌙";s:18:"グラムトン";s:3:"㌚";s:18:"クルゼイロ";s:3:"㌛";s:12:"クローネ";s:3:"㌜";s:9:"ケース";s:3:"㌝";s:9:"コルナ";s:3:"㌞";s:12:"コーポ";s:3:"㌟";s:12:"サイクル";s:3:"㌠";s:15:"サンチーム";s:3:"㌡";s:15:"シリング";s:3:"㌢";s:9:"センチ";s:3:"㌣";s:9:"セント";s:3:"㌤";s:12:"ダース";s:3:"㌥";s:9:"デシ";s:3:"㌦";s:9:"ドル";s:3:"㌧";s:6:"トン";s:3:"㌨";s:6:"ナノ";s:3:"㌩";s:9:"ノット";s:3:"㌪";s:9:"ハイツ";s:3:"㌫";s:18:"パーセント";s:3:"㌬";s:12:"パーツ";s:3:"㌭";s:15:"バーレル";s:3:"㌮";s:18:"ピアストル";s:3:"㌯";s:12:"ピクル";s:3:"㌰";s:9:"ピコ";s:3:"㌱";s:9:"ビル";s:3:"㌲";s:18:"ファラッド";s:3:"㌳";s:12:"フィート";s:3:"㌴";s:18:"ブッシェル";s:3:"㌵";s:9:"フラン";s:3:"㌶";s:15:"ヘクタール";s:3:"㌷";s:9:"ペソ";s:3:"㌸";s:12:"ペニヒ";s:3:"㌹";s:9:"ヘルツ";s:3:"㌺";s:12:"ペンス";s:3:"㌻";s:15:"ページ";s:3:"㌼";s:12:"ベータ";s:3:"㌽";s:15:"ポイント";s:3:"㌾";s:12:"ボルト";s:3:"㌿";s:6:"ホン";s:3:"㍀";s:15:"ポンド";s:3:"㍁";s:9:"ホール";s:3:"㍂";s:9:"ホーン";s:3:"㍃";s:12:"マイクロ";s:3:"㍄";s:9:"マイル";s:3:"㍅";s:9:"マッハ";s:3:"㍆";s:9:"マルク";s:3:"㍇";s:15:"マンション";s:3:"㍈";s:12:"ミクロン";s:3:"㍉";s:6:"ミリ";s:3:"㍊";s:18:"ミリバール";s:3:"㍋";s:9:"メガ";s:3:"㍌";s:15:"メガトン";s:3:"㍍";s:12:"メートル";s:3:"㍎";s:12:"ヤード";s:3:"㍏";s:9:"ヤール";s:3:"㍐";s:9:"ユアン";s:3:"㍑";s:12:"リットル";s:3:"㍒";s:6:"リラ";s:3:"㍓";s:12:"ルピー";s:3:"㍔";s:15:"ルーブル";s:3:"㍕";s:6:"レム";s:3:"㍖";s:18:"レントゲン";s:3:"㍗";s:9:"ワット";s:3:"㍘";s:4:"0点";s:3:"㍙";s:4:"1点";s:3:"㍚";s:4:"2点";s:3:"㍛";s:4:"3点";s:3:"㍜";s:4:"4点";s:3:"㍝";s:4:"5点";s:3:"㍞";s:4:"6点";s:3:"㍟";s:4:"7点";s:3:"㍠";s:4:"8点";s:3:"㍡";s:4:"9点";s:3:"㍢";s:5:"10点";s:3:"㍣";s:5:"11点";s:3:"㍤";s:5:"12点";s:3:"㍥";s:5:"13点";s:3:"㍦";s:5:"14点";s:3:"㍧";s:5:"15点";s:3:"㍨";s:5:"16点";s:3:"㍩";s:5:"17点";s:3:"㍪";s:5:"18点";s:3:"㍫";s:5:"19点";s:3:"㍬";s:5:"20点";s:3:"㍭";s:5:"21点";s:3:"㍮";s:5:"22点";s:3:"㍯";s:5:"23点";s:3:"㍰";s:5:"24点";s:3:"㍱";s:3:"hPa";s:3:"㍲";s:2:"da";s:3:"㍳";s:2:"AU";s:3:"㍴";s:3:"bar";s:3:"㍵";s:2:"oV";s:3:"㍶";s:2:"pc";s:3:"㍷";s:2:"dm";s:3:"㍸";s:3:"dm2";s:3:"㍹";s:3:"dm3";s:3:"㍺";s:2:"IU";s:3:"㍻";s:6:"平成";s:3:"㍼";s:6:"昭和";s:3:"㍽";s:6:"大正";s:3:"㍾";s:6:"明治";s:3:"㍿";s:12:"株式会社";s:3:"㎀";s:2:"pA";s:3:"㎁";s:2:"nA";s:3:"㎂";s:3:"μA";s:3:"㎃";s:2:"mA";s:3:"㎄";s:2:"kA";s:3:"㎅";s:2:"KB";s:3:"㎆";s:2:"MB";s:3:"㎇";s:2:"GB";s:3:"㎈";s:3:"cal";s:3:"㎉";s:4:"kcal";s:3:"㎊";s:2:"pF";s:3:"㎋";s:2:"nF";s:3:"㎌";s:3:"μF";s:3:"㎍";s:3:"μg";s:3:"㎎";s:2:"mg";s:3:"㎏";s:2:"kg";s:3:"㎐";s:2:"Hz";s:3:"㎑";s:3:"kHz";s:3:"㎒";s:3:"MHz";s:3:"㎓";s:3:"GHz";s:3:"㎔";s:3:"THz";s:3:"㎕";s:3:"μl";s:3:"㎖";s:2:"ml";s:3:"㎗";s:2:"dl";s:3:"㎘";s:2:"kl";s:3:"㎙";s:2:"fm";s:3:"㎚";s:2:"nm";s:3:"㎛";s:3:"μm";s:3:"㎜";s:2:"mm";s:3:"㎝";s:2:"cm";s:3:"㎞";s:2:"km";s:3:"㎟";s:3:"mm2";s:3:"㎠";s:3:"cm2";s:3:"㎡";s:2:"m2";s:3:"㎢";s:3:"km2";s:3:"㎣";s:3:"mm3";s:3:"㎤";s:3:"cm3";s:3:"㎥";s:2:"m3";s:3:"㎦";s:3:"km3";s:3:"㎧";s:5:"m∕s";s:3:"㎨";s:6:"m∕s2";s:3:"㎩";s:2:"Pa";s:3:"㎪";s:3:"kPa";s:3:"㎫";s:3:"MPa";s:3:"㎬";s:3:"GPa";s:3:"㎭";s:3:"rad";s:3:"㎮";s:7:"rad∕s";s:3:"㎯";s:8:"rad∕s2";s:3:"㎰";s:2:"ps";s:3:"㎱";s:2:"ns";s:3:"㎲";s:3:"μs";s:3:"㎳";s:2:"ms";s:3:"㎴";s:2:"pV";s:3:"㎵";s:2:"nV";s:3:"㎶";s:3:"μV";s:3:"㎷";s:2:"mV";s:3:"㎸";s:2:"kV";s:3:"㎹";s:2:"MV";s:3:"㎺";s:2:"pW";s:3:"㎻";s:2:"nW";s:3:"㎼";s:3:"μW";s:3:"㎽";s:2:"mW";s:3:"㎾";s:2:"kW";s:3:"㎿";s:2:"MW";s:3:"㏀";s:3:"kΩ";s:3:"㏁";s:3:"MΩ";s:3:"㏂";s:4:"a.m.";s:3:"㏃";s:2:"Bq";s:3:"㏄";s:2:"cc";s:3:"㏅";s:2:"cd";s:3:"㏆";s:6:"C∕kg";s:3:"㏇";s:3:"Co.";s:3:"㏈";s:2:"dB";s:3:"㏉";s:2:"Gy";s:3:"㏊";s:2:"ha";s:3:"㏋";s:2:"HP";s:3:"㏌";s:2:"in";s:3:"㏍";s:2:"KK";s:3:"㏎";s:2:"KM";s:3:"㏏";s:2:"kt";s:3:"㏐";s:2:"lm";s:3:"㏑";s:2:"ln";s:3:"㏒";s:3:"log";s:3:"㏓";s:2:"lx";s:3:"㏔";s:2:"mb";s:3:"㏕";s:3:"mil";s:3:"㏖";s:3:"mol";s:3:"㏗";s:2:"PH";s:3:"㏘";s:4:"p.m.";s:3:"㏙";s:3:"PPM";s:3:"㏚";s:2:"PR";s:3:"㏛";s:2:"sr";s:3:"㏜";s:2:"Sv";s:3:"㏝";s:2:"Wb";s:3:"㏞";s:5:"V∕m";s:3:"㏟";s:5:"A∕m";s:3:"㏠";s:4:"1日";s:3:"㏡";s:4:"2日";s:3:"㏢";s:4:"3日";s:3:"㏣";s:4:"4日";s:3:"㏤";s:4:"5日";s:3:"㏥";s:4:"6日";s:3:"㏦";s:4:"7日";s:3:"㏧";s:4:"8日";s:3:"㏨";s:4:"9日";s:3:"㏩";s:5:"10日";s:3:"㏪";s:5:"11日";s:3:"㏫";s:5:"12日";s:3:"㏬";s:5:"13日";s:3:"㏭";s:5:"14日";s:3:"㏮";s:5:"15日";s:3:"㏯";s:5:"16日";s:3:"㏰";s:5:"17日";s:3:"㏱";s:5:"18日";s:3:"㏲";s:5:"19日";s:3:"㏳";s:5:"20日";s:3:"㏴";s:5:"21日";s:3:"㏵";s:5:"22日";s:3:"㏶";s:5:"23日";s:3:"㏷";s:5:"24日";s:3:"㏸";s:5:"25日";s:3:"㏹";s:5:"26日";s:3:"㏺";s:5:"27日";s:3:"㏻";s:5:"28日";s:3:"㏼";s:5:"29日";s:3:"㏽";s:5:"30日";s:3:"㏾";s:5:"31日";s:3:"㏿";s:3:"gal";s:3:"豈";s:3:"豈";s:3:"更";s:3:"更";s:3:"車";s:3:"車";s:3:"賈";s:3:"賈";s:3:"滑";s:3:"滑";s:3:"串";s:3:"串";s:3:"句";s:3:"句";s:3:"龜";s:3:"龜";s:3:"龜";s:3:"龜";s:3:"契";s:3:"契";s:3:"金";s:3:"金";s:3:"喇";s:3:"喇";s:3:"奈";s:3:"奈";s:3:"懶";s:3:"懶";s:3:"癩";s:3:"癩";s:3:"羅";s:3:"羅";s:3:"蘿";s:3:"蘿";s:3:"螺";s:3:"螺";s:3:"裸";s:3:"裸";s:3:"邏";s:3:"邏";s:3:"樂";s:3:"樂";s:3:"洛";s:3:"洛";s:3:"烙";s:3:"烙";s:3:"珞";s:3:"珞";s:3:"落";s:3:"落";s:3:"酪";s:3:"酪";s:3:"駱";s:3:"駱";s:3:"亂";s:3:"亂";s:3:"卵";s:3:"卵";s:3:"欄";s:3:"欄";s:3:"爛";s:3:"爛";s:3:"蘭";s:3:"蘭";s:3:"鸞";s:3:"鸞";s:3:"嵐";s:3:"嵐";s:3:"濫";s:3:"濫";s:3:"藍";s:3:"藍";s:3:"襤";s:3:"襤";s:3:"拉";s:3:"拉";s:3:"臘";s:3:"臘";s:3:"蠟";s:3:"蠟";s:3:"廊";s:3:"廊";s:3:"朗";s:3:"朗";s:3:"浪";s:3:"浪";s:3:"狼";s:3:"狼";s:3:"郎";s:3:"郎";s:3:"來";s:3:"來";s:3:"冷";s:3:"冷";s:3:"勞";s:3:"勞";s:3:"擄";s:3:"擄";s:3:"櫓";s:3:"櫓";s:3:"爐";s:3:"爐";s:3:"盧";s:3:"盧";s:3:"老";s:3:"老";s:3:"蘆";s:3:"蘆";s:3:"虜";s:3:"虜";s:3:"路";s:3:"路";s:3:"露";s:3:"露";s:3:"魯";s:3:"魯";s:3:"鷺";s:3:"鷺";s:3:"碌";s:3:"碌";s:3:"祿";s:3:"祿";s:3:"綠";s:3:"綠";s:3:"菉";s:3:"菉";s:3:"錄";s:3:"錄";s:3:"鹿";s:3:"鹿";s:3:"論";s:3:"論";s:3:"壟";s:3:"壟";s:3:"弄";s:3:"弄";s:3:"籠";s:3:"籠";s:3:"聾";s:3:"聾";s:3:"牢";s:3:"牢";s:3:"磊";s:3:"磊";s:3:"賂";s:3:"賂";s:3:"雷";s:3:"雷";s:3:"壘";s:3:"壘";s:3:"屢";s:3:"屢";s:3:"樓";s:3:"樓";s:3:"淚";s:3:"淚";s:3:"漏";s:3:"漏";s:3:"累";s:3:"累";s:3:"縷";s:3:"縷";s:3:"陋";s:3:"陋";s:3:"勒";s:3:"勒";s:3:"肋";s:3:"肋";s:3:"凜";s:3:"凜";s:3:"凌";s:3:"凌";s:3:"稜";s:3:"稜";s:3:"綾";s:3:"綾";s:3:"菱";s:3:"菱";s:3:"陵";s:3:"陵";s:3:"讀";s:3:"讀";s:3:"拏";s:3:"拏";s:3:"樂";s:3:"樂";s:3:"諾";s:3:"諾";s:3:"丹";s:3:"丹";s:3:"寧";s:3:"寧";s:3:"怒";s:3:"怒";s:3:"率";s:3:"率";s:3:"異";s:3:"異";s:3:"北";s:3:"北";s:3:"磻";s:3:"磻";s:3:"便";s:3:"便";s:3:"復";s:3:"復";s:3:"不";s:3:"不";s:3:"泌";s:3:"泌";s:3:"數";s:3:"數";s:3:"索";s:3:"索";s:3:"參";s:3:"參";s:3:"塞";s:3:"塞";s:3:"省";s:3:"省";s:3:"葉";s:3:"葉";s:3:"說";s:3:"說";s:3:"殺";s:3:"殺";s:3:"辰";s:3:"辰";s:3:"沈";s:3:"沈";s:3:"拾";s:3:"拾";s:3:"若";s:3:"若";s:3:"掠";s:3:"掠";s:3:"略";s:3:"略";s:3:"亮";s:3:"亮";s:3:"兩";s:3:"兩";s:3:"凉";s:3:"凉";s:3:"梁";s:3:"梁";s:3:"糧";s:3:"糧";s:3:"良";s:3:"良";s:3:"諒";s:3:"諒";s:3:"量";s:3:"量";s:3:"勵";s:3:"勵";s:3:"呂";s:3:"呂";s:3:"女";s:3:"女";s:3:"廬";s:3:"廬";s:3:"旅";s:3:"旅";s:3:"濾";s:3:"濾";s:3:"礪";s:3:"礪";s:3:"閭";s:3:"閭";s:3:"驪";s:3:"驪";s:3:"麗";s:3:"麗";s:3:"黎";s:3:"黎";s:3:"力";s:3:"力";s:3:"曆";s:3:"曆";s:3:"歷";s:3:"歷";s:3:"轢";s:3:"轢";s:3:"年";s:3:"年";s:3:"憐";s:3:"憐";s:3:"戀";s:3:"戀";s:3:"撚";s:3:"撚";s:3:"漣";s:3:"漣";s:3:"煉";s:3:"煉";s:3:"璉";s:3:"璉";s:3:"秊";s:3:"秊";s:3:"練";s:3:"練";s:3:"聯";s:3:"聯";s:3:"輦";s:3:"輦";s:3:"蓮";s:3:"蓮";s:3:"連";s:3:"連";s:3:"鍊";s:3:"鍊";s:3:"列";s:3:"列";s:3:"劣";s:3:"劣";s:3:"咽";s:3:"咽";s:3:"烈";s:3:"烈";s:3:"裂";s:3:"裂";s:3:"說";s:3:"說";s:3:"廉";s:3:"廉";s:3:"念";s:3:"念";s:3:"捻";s:3:"捻";s:3:"殮";s:3:"殮";s:3:"簾";s:3:"簾";s:3:"獵";s:3:"獵";s:3:"令";s:3:"令";s:3:"囹";s:3:"囹";s:3:"寧";s:3:"寧";s:3:"嶺";s:3:"嶺";s:3:"怜";s:3:"怜";s:3:"玲";s:3:"玲";s:3:"瑩";s:3:"瑩";s:3:"羚";s:3:"羚";s:3:"聆";s:3:"聆";s:3:"鈴";s:3:"鈴";s:3:"零";s:3:"零";s:3:"靈";s:3:"靈";s:3:"領";s:3:"領";s:3:"例";s:3:"例";s:3:"禮";s:3:"禮";s:3:"醴";s:3:"醴";s:3:"隸";s:3:"隸";s:3:"惡";s:3:"惡";s:3:"了";s:3:"了";s:3:"僚";s:3:"僚";s:3:"寮";s:3:"寮";s:3:"尿";s:3:"尿";s:3:"料";s:3:"料";s:3:"樂";s:3:"樂";s:3:"燎";s:3:"燎";s:3:"療";s:3:"療";s:3:"蓼";s:3:"蓼";s:3:"遼";s:3:"遼";s:3:"龍";s:3:"龍";s:3:"暈";s:3:"暈";s:3:"阮";s:3:"阮";s:3:"劉";s:3:"劉";s:3:"杻";s:3:"杻";s:3:"柳";s:3:"柳";s:3:"流";s:3:"流";s:3:"溜";s:3:"溜";s:3:"琉";s:3:"琉";s:3:"留";s:3:"留";s:3:"硫";s:3:"硫";s:3:"紐";s:3:"紐";s:3:"類";s:3:"類";s:3:"六";s:3:"六";s:3:"戮";s:3:"戮";s:3:"陸";s:3:"陸";s:3:"倫";s:3:"倫";s:3:"崙";s:3:"崙";s:3:"淪";s:3:"淪";s:3:"輪";s:3:"輪";s:3:"律";s:3:"律";s:3:"慄";s:3:"慄";s:3:"栗";s:3:"栗";s:3:"率";s:3:"率";s:3:"隆";s:3:"隆";s:3:"利";s:3:"利";s:3:"吏";s:3:"吏";s:3:"履";s:3:"履";s:3:"易";s:3:"易";s:3:"李";s:3:"李";s:3:"梨";s:3:"梨";s:3:"泥";s:3:"泥";s:3:"理";s:3:"理";s:3:"痢";s:3:"痢";s:3:"罹";s:3:"罹";s:3:"裏";s:3:"裏";s:3:"裡";s:3:"裡";s:3:"里";s:3:"里";s:3:"離";s:3:"離";s:3:"匿";s:3:"匿";s:3:"溺";s:3:"溺";s:3:"吝";s:3:"吝";s:3:"燐";s:3:"燐";s:3:"璘";s:3:"璘";s:3:"藺";s:3:"藺";s:3:"隣";s:3:"隣";s:3:"鱗";s:3:"鱗";s:3:"麟";s:3:"麟";s:3:"林";s:3:"林";s:3:"淋";s:3:"淋";s:3:"臨";s:3:"臨";s:3:"立";s:3:"立";s:3:"笠";s:3:"笠";s:3:"粒";s:3:"粒";s:3:"狀";s:3:"狀";s:3:"炙";s:3:"炙";s:3:"識";s:3:"識";s:3:"什";s:3:"什";s:3:"茶";s:3:"茶";s:3:"刺";s:3:"刺";s:3:"切";s:3:"切";s:3:"度";s:3:"度";s:3:"拓";s:3:"拓";s:3:"糖";s:3:"糖";s:3:"宅";s:3:"宅";s:3:"洞";s:3:"洞";s:3:"暴";s:3:"暴";s:3:"輻";s:3:"輻";s:3:"行";s:3:"行";s:3:"降";s:3:"降";s:3:"見";s:3:"見";s:3:"廓";s:3:"廓";s:3:"兀";s:3:"兀";s:3:"嗀";s:3:"嗀";s:3:"塚";s:3:"塚";s:3:"晴";s:3:"晴";s:3:"凞";s:3:"凞";s:3:"猪";s:3:"猪";s:3:"益";s:3:"益";s:3:"礼";s:3:"礼";s:3:"神";s:3:"神";s:3:"祥";s:3:"祥";s:3:"福";s:3:"福";s:3:"靖";s:3:"靖";s:3:"精";s:3:"精";s:3:"羽";s:3:"羽";s:3:"蘒";s:3:"蘒";s:3:"諸";s:3:"諸";s:3:"逸";s:3:"逸";s:3:"都";s:3:"都";s:3:"飯";s:3:"飯";s:3:"飼";s:3:"飼";s:3:"館";s:3:"館";s:3:"鶴";s:3:"鶴";s:3:"侮";s:3:"侮";s:3:"僧";s:3:"僧";s:3:"免";s:3:"免";s:3:"勉";s:3:"勉";s:3:"勤";s:3:"勤";s:3:"卑";s:3:"卑";s:3:"喝";s:3:"喝";s:3:"嘆";s:3:"嘆";s:3:"器";s:3:"器";s:3:"塀";s:3:"塀";s:3:"墨";s:3:"墨";s:3:"層";s:3:"層";s:3:"屮";s:3:"屮";s:3:"悔";s:3:"悔";s:3:"慨";s:3:"慨";s:3:"憎";s:3:"憎";s:3:"懲";s:3:"懲";s:3:"敏";s:3:"敏";s:3:"既";s:3:"既";s:3:"暑";s:3:"暑";s:3:"梅";s:3:"梅";s:3:"海";s:3:"海";s:3:"渚";s:3:"渚";s:3:"漢";s:3:"漢";s:3:"煮";s:3:"煮";s:3:"爫";s:3:"爫";s:3:"琢";s:3:"琢";s:3:"碑";s:3:"碑";s:3:"社";s:3:"社";s:3:"祉";s:3:"祉";s:3:"祈";s:3:"祈";s:3:"祐";s:3:"祐";s:3:"祖";s:3:"祖";s:3:"祝";s:3:"祝";s:3:"禍";s:3:"禍";s:3:"禎";s:3:"禎";s:3:"穀";s:3:"穀";s:3:"突";s:3:"突";s:3:"節";s:3:"節";s:3:"練";s:3:"練";s:3:"縉";s:3:"縉";s:3:"繁";s:3:"繁";s:3:"署";s:3:"署";s:3:"者";s:3:"者";s:3:"臭";s:3:"臭";s:3:"艹";s:3:"艹";s:3:"艹";s:3:"艹";s:3:"著";s:3:"著";s:3:"褐";s:3:"褐";s:3:"視";s:3:"視";s:3:"謁";s:3:"謁";s:3:"謹";s:3:"謹";s:3:"賓";s:3:"賓";s:3:"贈";s:3:"贈";s:3:"辶";s:3:"辶";s:3:"逸";s:3:"逸";s:3:"難";s:3:"難";s:3:"響";s:3:"響";s:3:"頻";s:3:"頻";s:3:"並";s:3:"並";s:3:"况";s:3:"况";s:3:"全";s:3:"全";s:3:"侀";s:3:"侀";s:3:"充";s:3:"充";s:3:"冀";s:3:"冀";s:3:"勇";s:3:"勇";s:3:"勺";s:3:"勺";s:3:"喝";s:3:"喝";s:3:"啕";s:3:"啕";s:3:"喙";s:3:"喙";s:3:"嗢";s:3:"嗢";s:3:"塚";s:3:"塚";s:3:"墳";s:3:"墳";s:3:"奄";s:3:"奄";s:3:"奔";s:3:"奔";s:3:"婢";s:3:"婢";s:3:"嬨";s:3:"嬨";s:3:"廒";s:3:"廒";s:3:"廙";s:3:"廙";s:3:"彩";s:3:"彩";s:3:"徭";s:3:"徭";s:3:"惘";s:3:"惘";s:3:"慎";s:3:"慎";s:3:"愈";s:3:"愈";s:3:"憎";s:3:"憎";s:3:"慠";s:3:"慠";s:3:"懲";s:3:"懲";s:3:"戴";s:3:"戴";s:3:"揄";s:3:"揄";s:3:"搜";s:3:"搜";s:3:"摒";s:3:"摒";s:3:"敖";s:3:"敖";s:3:"晴";s:3:"晴";s:3:"朗";s:3:"朗";s:3:"望";s:3:"望";s:3:"杖";s:3:"杖";s:3:"歹";s:3:"歹";s:3:"殺";s:3:"殺";s:3:"流";s:3:"流";s:3:"滛";s:3:"滛";s:3:"滋";s:3:"滋";s:3:"漢";s:3:"漢";s:3:"瀞";s:3:"瀞";s:3:"煮";s:3:"煮";s:3:"瞧";s:3:"瞧";s:3:"爵";s:3:"爵";s:3:"犯";s:3:"犯";s:3:"猪";s:3:"猪";s:3:"瑱";s:3:"瑱";s:3:"甆";s:3:"甆";s:3:"画";s:3:"画";s:3:"瘝";s:3:"瘝";s:3:"瘟";s:3:"瘟";s:3:"益";s:3:"益";s:3:"盛";s:3:"盛";s:3:"直";s:3:"直";s:3:"睊";s:3:"睊";s:3:"着";s:3:"着";s:3:"磌";s:3:"磌";s:3:"窱";s:3:"窱";s:3:"節";s:3:"節";s:3:"类";s:3:"类";s:3:"絛";s:3:"絛";s:3:"練";s:3:"練";s:3:"缾";s:3:"缾";s:3:"者";s:3:"者";s:3:"荒";s:3:"荒";s:3:"華";s:3:"華";s:3:"蝹";s:3:"蝹";s:3:"襁";s:3:"襁";s:3:"覆";s:3:"覆";s:3:"視";s:3:"視";s:3:"調";s:3:"調";s:3:"諸";s:3:"諸";s:3:"請";s:3:"請";s:3:"謁";s:3:"謁";s:3:"諾";s:3:"諾";s:3:"諭";s:3:"諭";s:3:"謹";s:3:"謹";s:3:"變";s:3:"變";s:3:"贈";s:3:"贈";s:3:"輸";s:3:"輸";s:3:"遲";s:3:"遲";s:3:"醙";s:3:"醙";s:3:"鉶";s:3:"鉶";s:3:"陼";s:3:"陼";s:3:"難";s:3:"難";s:3:"靖";s:3:"靖";s:3:"韛";s:3:"韛";s:3:"響";s:3:"響";s:3:"頋";s:3:"頋";s:3:"頻";s:3:"頻";s:3:"鬒";s:3:"鬒";s:3:"龜";s:3:"龜";s:3:"𢡊";s:4:"𢡊";s:3:"𢡄";s:4:"𢡄";s:3:"𣏕";s:4:"𣏕";s:3:"㮝";s:3:"㮝";s:3:"䀘";s:3:"䀘";s:3:"䀹";s:3:"䀹";s:3:"𥉉";s:4:"𥉉";s:3:"𥳐";s:4:"𥳐";s:3:"𧻓";s:4:"𧻓";s:3:"齃";s:3:"齃";s:3:"龎";s:3:"龎";s:3:"ff";s:2:"ff";s:3:"fi";s:2:"fi";s:3:"fl";s:2:"fl";s:3:"ffi";s:3:"ffi";s:3:"ffl";s:3:"ffl";s:3:"ſt";s:2:"st";s:3:"st";s:2:"st";s:3:"ﬓ";s:4:"մն";s:3:"ﬔ";s:4:"մե";s:3:"ﬕ";s:4:"մի";s:3:"ﬖ";s:4:"վն";s:3:"ﬗ";s:4:"մխ";s:3:"יִ";s:4:"יִ";s:3:"ײַ";s:4:"ײַ";s:3:"ﬠ";s:2:"ע";s:3:"ﬡ";s:2:"א";s:3:"ﬢ";s:2:"ד";s:3:"ﬣ";s:2:"ה";s:3:"ﬤ";s:2:"כ";s:3:"ﬥ";s:2:"ל";s:3:"ﬦ";s:2:"ם";s:3:"ﬧ";s:2:"ר";s:3:"ﬨ";s:2:"ת";s:3:"﬩";s:1:"+";s:3:"שׁ";s:4:"שׁ";s:3:"שׂ";s:4:"שׂ";s:3:"שּׁ";s:6:"שּׁ";s:3:"שּׂ";s:6:"שּׂ";s:3:"אַ";s:4:"אַ";s:3:"אָ";s:4:"אָ";s:3:"אּ";s:4:"אּ";s:3:"בּ";s:4:"בּ";s:3:"גּ";s:4:"גּ";s:3:"דּ";s:4:"דּ";s:3:"הּ";s:4:"הּ";s:3:"וּ";s:4:"וּ";s:3:"זּ";s:4:"זּ";s:3:"טּ";s:4:"טּ";s:3:"יּ";s:4:"יּ";s:3:"ךּ";s:4:"ךּ";s:3:"כּ";s:4:"כּ";s:3:"לּ";s:4:"לּ";s:3:"מּ";s:4:"מּ";s:3:"נּ";s:4:"נּ";s:3:"סּ";s:4:"סּ";s:3:"ףּ";s:4:"ףּ";s:3:"פּ";s:4:"פּ";s:3:"צּ";s:4:"צּ";s:3:"קּ";s:4:"קּ";s:3:"רּ";s:4:"רּ";s:3:"שּ";s:4:"שּ";s:3:"תּ";s:4:"תּ";s:3:"וֹ";s:4:"וֹ";s:3:"בֿ";s:4:"בֿ";s:3:"כֿ";s:4:"כֿ";s:3:"פֿ";s:4:"פֿ";s:3:"ﭏ";s:4:"אל";s:3:"ﭐ";s:2:"ٱ";s:3:"ﭑ";s:2:"ٱ";s:3:"ﭒ";s:2:"ٻ";s:3:"ﭓ";s:2:"ٻ";s:3:"ﭔ";s:2:"ٻ";s:3:"ﭕ";s:2:"ٻ";s:3:"ﭖ";s:2:"پ";s:3:"ﭗ";s:2:"پ";s:3:"ﭘ";s:2:"پ";s:3:"ﭙ";s:2:"پ";s:3:"ﭚ";s:2:"ڀ";s:3:"ﭛ";s:2:"ڀ";s:3:"ﭜ";s:2:"ڀ";s:3:"ﭝ";s:2:"ڀ";s:3:"ﭞ";s:2:"ٺ";s:3:"ﭟ";s:2:"ٺ";s:3:"ﭠ";s:2:"ٺ";s:3:"ﭡ";s:2:"ٺ";s:3:"ﭢ";s:2:"ٿ";s:3:"ﭣ";s:2:"ٿ";s:3:"ﭤ";s:2:"ٿ";s:3:"ﭥ";s:2:"ٿ";s:3:"ﭦ";s:2:"ٹ";s:3:"ﭧ";s:2:"ٹ";s:3:"ﭨ";s:2:"ٹ";s:3:"ﭩ";s:2:"ٹ";s:3:"ﭪ";s:2:"ڤ";s:3:"ﭫ";s:2:"ڤ";s:3:"ﭬ";s:2:"ڤ";s:3:"ﭭ";s:2:"ڤ";s:3:"ﭮ";s:2:"ڦ";s:3:"ﭯ";s:2:"ڦ";s:3:"ﭰ";s:2:"ڦ";s:3:"ﭱ";s:2:"ڦ";s:3:"ﭲ";s:2:"ڄ";s:3:"ﭳ";s:2:"ڄ";s:3:"ﭴ";s:2:"ڄ";s:3:"ﭵ";s:2:"ڄ";s:3:"ﭶ";s:2:"ڃ";s:3:"ﭷ";s:2:"ڃ";s:3:"ﭸ";s:2:"ڃ";s:3:"ﭹ";s:2:"ڃ";s:3:"ﭺ";s:2:"چ";s:3:"ﭻ";s:2:"چ";s:3:"ﭼ";s:2:"چ";s:3:"ﭽ";s:2:"چ";s:3:"ﭾ";s:2:"ڇ";s:3:"ﭿ";s:2:"ڇ";s:3:"ﮀ";s:2:"ڇ";s:3:"ﮁ";s:2:"ڇ";s:3:"ﮂ";s:2:"ڍ";s:3:"ﮃ";s:2:"ڍ";s:3:"ﮄ";s:2:"ڌ";s:3:"ﮅ";s:2:"ڌ";s:3:"ﮆ";s:2:"ڎ";s:3:"ﮇ";s:2:"ڎ";s:3:"ﮈ";s:2:"ڈ";s:3:"ﮉ";s:2:"ڈ";s:3:"ﮊ";s:2:"ژ";s:3:"ﮋ";s:2:"ژ";s:3:"ﮌ";s:2:"ڑ";s:3:"ﮍ";s:2:"ڑ";s:3:"ﮎ";s:2:"ک";s:3:"ﮏ";s:2:"ک";s:3:"ﮐ";s:2:"ک";s:3:"ﮑ";s:2:"ک";s:3:"ﮒ";s:2:"گ";s:3:"ﮓ";s:2:"گ";s:3:"ﮔ";s:2:"گ";s:3:"ﮕ";s:2:"گ";s:3:"ﮖ";s:2:"ڳ";s:3:"ﮗ";s:2:"ڳ";s:3:"ﮘ";s:2:"ڳ";s:3:"ﮙ";s:2:"ڳ";s:3:"ﮚ";s:2:"ڱ";s:3:"ﮛ";s:2:"ڱ";s:3:"ﮜ";s:2:"ڱ";s:3:"ﮝ";s:2:"ڱ";s:3:"ﮞ";s:2:"ں";s:3:"ﮟ";s:2:"ں";s:3:"ﮠ";s:2:"ڻ";s:3:"ﮡ";s:2:"ڻ";s:3:"ﮢ";s:2:"ڻ";s:3:"ﮣ";s:2:"ڻ";s:3:"ﮤ";s:4:"ۀ";s:3:"ﮥ";s:4:"ۀ";s:3:"ﮦ";s:2:"ہ";s:3:"ﮧ";s:2:"ہ";s:3:"ﮨ";s:2:"ہ";s:3:"ﮩ";s:2:"ہ";s:3:"ﮪ";s:2:"ھ";s:3:"ﮫ";s:2:"ھ";s:3:"ﮬ";s:2:"ھ";s:3:"ﮭ";s:2:"ھ";s:3:"ﮮ";s:2:"ے";s:3:"ﮯ";s:2:"ے";s:3:"ﮰ";s:4:"ۓ";s:3:"ﮱ";s:4:"ۓ";s:3:"ﯓ";s:2:"ڭ";s:3:"ﯔ";s:2:"ڭ";s:3:"ﯕ";s:2:"ڭ";s:3:"ﯖ";s:2:"ڭ";s:3:"ﯗ";s:2:"ۇ";s:3:"ﯘ";s:2:"ۇ";s:3:"ﯙ";s:2:"ۆ";s:3:"ﯚ";s:2:"ۆ";s:3:"ﯛ";s:2:"ۈ";s:3:"ﯜ";s:2:"ۈ";s:3:"ﯝ";s:4:"ۇٴ";s:3:"ﯞ";s:2:"ۋ";s:3:"ﯟ";s:2:"ۋ";s:3:"ﯠ";s:2:"ۅ";s:3:"ﯡ";s:2:"ۅ";s:3:"ﯢ";s:2:"ۉ";s:3:"ﯣ";s:2:"ۉ";s:3:"ﯤ";s:2:"ې";s:3:"ﯥ";s:2:"ې";s:3:"ﯦ";s:2:"ې";s:3:"ﯧ";s:2:"ې";s:3:"ﯨ";s:2:"ى";s:3:"ﯩ";s:2:"ى";s:3:"ﯪ";s:6:"ئا";s:3:"ﯫ";s:6:"ئا";s:3:"ﯬ";s:6:"ئە";s:3:"ﯭ";s:6:"ئە";s:3:"ﯮ";s:6:"ئو";s:3:"ﯯ";s:6:"ئو";s:3:"ﯰ";s:6:"ئۇ";s:3:"ﯱ";s:6:"ئۇ";s:3:"ﯲ";s:6:"ئۆ";s:3:"ﯳ";s:6:"ئۆ";s:3:"ﯴ";s:6:"ئۈ";s:3:"ﯵ";s:6:"ئۈ";s:3:"ﯶ";s:6:"ئې";s:3:"ﯷ";s:6:"ئې";s:3:"ﯸ";s:6:"ئې";s:3:"ﯹ";s:6:"ئى";s:3:"ﯺ";s:6:"ئى";s:3:"ﯻ";s:6:"ئى";s:3:"ﯼ";s:2:"ی";s:3:"ﯽ";s:2:"ی";s:3:"ﯾ";s:2:"ی";s:3:"ﯿ";s:2:"ی";s:3:"ﰀ";s:6:"ئج";s:3:"ﰁ";s:6:"ئح";s:3:"ﰂ";s:6:"ئم";s:3:"ﰃ";s:6:"ئى";s:3:"ﰄ";s:6:"ئي";s:3:"ﰅ";s:4:"بج";s:3:"ﰆ";s:4:"بح";s:3:"ﰇ";s:4:"بخ";s:3:"ﰈ";s:4:"بم";s:3:"ﰉ";s:4:"بى";s:3:"ﰊ";s:4:"بي";s:3:"ﰋ";s:4:"تج";s:3:"ﰌ";s:4:"تح";s:3:"ﰍ";s:4:"تخ";s:3:"ﰎ";s:4:"تم";s:3:"ﰏ";s:4:"تى";s:3:"ﰐ";s:4:"تي";s:3:"ﰑ";s:4:"ثج";s:3:"ﰒ";s:4:"ثم";s:3:"ﰓ";s:4:"ثى";s:3:"ﰔ";s:4:"ثي";s:3:"ﰕ";s:4:"جح";s:3:"ﰖ";s:4:"جم";s:3:"ﰗ";s:4:"حج";s:3:"ﰘ";s:4:"حم";s:3:"ﰙ";s:4:"خج";s:3:"ﰚ";s:4:"خح";s:3:"ﰛ";s:4:"خم";s:3:"ﰜ";s:4:"سج";s:3:"ﰝ";s:4:"سح";s:3:"ﰞ";s:4:"سخ";s:3:"ﰟ";s:4:"سم";s:3:"ﰠ";s:4:"صح";s:3:"ﰡ";s:4:"صم";s:3:"ﰢ";s:4:"ضج";s:3:"ﰣ";s:4:"ضح";s:3:"ﰤ";s:4:"ضخ";s:3:"ﰥ";s:4:"ضم";s:3:"ﰦ";s:4:"طح";s:3:"ﰧ";s:4:"طم";s:3:"ﰨ";s:4:"ظم";s:3:"ﰩ";s:4:"عج";s:3:"ﰪ";s:4:"عم";s:3:"ﰫ";s:4:"غج";s:3:"ﰬ";s:4:"غم";s:3:"ﰭ";s:4:"فج";s:3:"ﰮ";s:4:"فح";s:3:"ﰯ";s:4:"فخ";s:3:"ﰰ";s:4:"فم";s:3:"ﰱ";s:4:"فى";s:3:"ﰲ";s:4:"في";s:3:"ﰳ";s:4:"قح";s:3:"ﰴ";s:4:"قم";s:3:"ﰵ";s:4:"قى";s:3:"ﰶ";s:4:"قي";s:3:"ﰷ";s:4:"كا";s:3:"ﰸ";s:4:"كج";s:3:"ﰹ";s:4:"كح";s:3:"ﰺ";s:4:"كخ";s:3:"ﰻ";s:4:"كل";s:3:"ﰼ";s:4:"كم";s:3:"ﰽ";s:4:"كى";s:3:"ﰾ";s:4:"كي";s:3:"ﰿ";s:4:"لج";s:3:"ﱀ";s:4:"لح";s:3:"ﱁ";s:4:"لخ";s:3:"ﱂ";s:4:"لم";s:3:"ﱃ";s:4:"لى";s:3:"ﱄ";s:4:"لي";s:3:"ﱅ";s:4:"مج";s:3:"ﱆ";s:4:"مح";s:3:"ﱇ";s:4:"مخ";s:3:"ﱈ";s:4:"مم";s:3:"ﱉ";s:4:"مى";s:3:"ﱊ";s:4:"مي";s:3:"ﱋ";s:4:"نج";s:3:"ﱌ";s:4:"نح";s:3:"ﱍ";s:4:"نخ";s:3:"ﱎ";s:4:"نم";s:3:"ﱏ";s:4:"نى";s:3:"ﱐ";s:4:"ني";s:3:"ﱑ";s:4:"هج";s:3:"ﱒ";s:4:"هم";s:3:"ﱓ";s:4:"هى";s:3:"ﱔ";s:4:"هي";s:3:"ﱕ";s:4:"يج";s:3:"ﱖ";s:4:"يح";s:3:"ﱗ";s:4:"يخ";s:3:"ﱘ";s:4:"يم";s:3:"ﱙ";s:4:"يى";s:3:"ﱚ";s:4:"يي";s:3:"ﱛ";s:4:"ذٰ";s:3:"ﱜ";s:4:"رٰ";s:3:"ﱝ";s:4:"ىٰ";s:3:"ﱞ";s:5:" ٌّ";s:3:"ﱟ";s:5:" ٍّ";s:3:"ﱠ";s:5:" َّ";s:3:"ﱡ";s:5:" ُّ";s:3:"ﱢ";s:5:" ِّ";s:3:"ﱣ";s:5:" ّٰ";s:3:"ﱤ";s:6:"ئر";s:3:"ﱥ";s:6:"ئز";s:3:"ﱦ";s:6:"ئم";s:3:"ﱧ";s:6:"ئن";s:3:"ﱨ";s:6:"ئى";s:3:"ﱩ";s:6:"ئي";s:3:"ﱪ";s:4:"بر";s:3:"ﱫ";s:4:"بز";s:3:"ﱬ";s:4:"بم";s:3:"ﱭ";s:4:"بن";s:3:"ﱮ";s:4:"بى";s:3:"ﱯ";s:4:"بي";s:3:"ﱰ";s:4:"تر";s:3:"ﱱ";s:4:"تز";s:3:"ﱲ";s:4:"تم";s:3:"ﱳ";s:4:"تن";s:3:"ﱴ";s:4:"تى";s:3:"ﱵ";s:4:"تي";s:3:"ﱶ";s:4:"ثر";s:3:"ﱷ";s:4:"ثز";s:3:"ﱸ";s:4:"ثم";s:3:"ﱹ";s:4:"ثن";s:3:"ﱺ";s:4:"ثى";s:3:"ﱻ";s:4:"ثي";s:3:"ﱼ";s:4:"فى";s:3:"ﱽ";s:4:"في";s:3:"ﱾ";s:4:"قى";s:3:"ﱿ";s:4:"قي";s:3:"ﲀ";s:4:"كا";s:3:"ﲁ";s:4:"كل";s:3:"ﲂ";s:4:"كم";s:3:"ﲃ";s:4:"كى";s:3:"ﲄ";s:4:"كي";s:3:"ﲅ";s:4:"لم";s:3:"ﲆ";s:4:"لى";s:3:"ﲇ";s:4:"لي";s:3:"ﲈ";s:4:"ما";s:3:"ﲉ";s:4:"مم";s:3:"ﲊ";s:4:"نر";s:3:"ﲋ";s:4:"نز";s:3:"ﲌ";s:4:"نم";s:3:"ﲍ";s:4:"نن";s:3:"ﲎ";s:4:"نى";s:3:"ﲏ";s:4:"ني";s:3:"ﲐ";s:4:"ىٰ";s:3:"ﲑ";s:4:"ير";s:3:"ﲒ";s:4:"يز";s:3:"ﲓ";s:4:"يم";s:3:"ﲔ";s:4:"ين";s:3:"ﲕ";s:4:"يى";s:3:"ﲖ";s:4:"يي";s:3:"ﲗ";s:6:"ئج";s:3:"ﲘ";s:6:"ئح";s:3:"ﲙ";s:6:"ئخ";s:3:"ﲚ";s:6:"ئم";s:3:"ﲛ";s:6:"ئه";s:3:"ﲜ";s:4:"بج";s:3:"ﲝ";s:4:"بح";s:3:"ﲞ";s:4:"بخ";s:3:"ﲟ";s:4:"بم";s:3:"ﲠ";s:4:"به";s:3:"ﲡ";s:4:"تج";s:3:"ﲢ";s:4:"تح";s:3:"ﲣ";s:4:"تخ";s:3:"ﲤ";s:4:"تم";s:3:"ﲥ";s:4:"ته";s:3:"ﲦ";s:4:"ثم";s:3:"ﲧ";s:4:"جح";s:3:"ﲨ";s:4:"جم";s:3:"ﲩ";s:4:"حج";s:3:"ﲪ";s:4:"حم";s:3:"ﲫ";s:4:"خج";s:3:"ﲬ";s:4:"خم";s:3:"ﲭ";s:4:"سج";s:3:"ﲮ";s:4:"سح";s:3:"ﲯ";s:4:"سخ";s:3:"ﲰ";s:4:"سم";s:3:"ﲱ";s:4:"صح";s:3:"ﲲ";s:4:"صخ";s:3:"ﲳ";s:4:"صم";s:3:"ﲴ";s:4:"ضج";s:3:"ﲵ";s:4:"ضح";s:3:"ﲶ";s:4:"ضخ";s:3:"ﲷ";s:4:"ضم";s:3:"ﲸ";s:4:"طح";s:3:"ﲹ";s:4:"ظم";s:3:"ﲺ";s:4:"عج";s:3:"ﲻ";s:4:"عم";s:3:"ﲼ";s:4:"غج";s:3:"ﲽ";s:4:"غم";s:3:"ﲾ";s:4:"فج";s:3:"ﲿ";s:4:"فح";s:3:"ﳀ";s:4:"فخ";s:3:"ﳁ";s:4:"فم";s:3:"ﳂ";s:4:"قح";s:3:"ﳃ";s:4:"قم";s:3:"ﳄ";s:4:"كج";s:3:"ﳅ";s:4:"كح";s:3:"ﳆ";s:4:"كخ";s:3:"ﳇ";s:4:"كل";s:3:"ﳈ";s:4:"كم";s:3:"ﳉ";s:4:"لج";s:3:"ﳊ";s:4:"لح";s:3:"ﳋ";s:4:"لخ";s:3:"ﳌ";s:4:"لم";s:3:"ﳍ";s:4:"له";s:3:"ﳎ";s:4:"مج";s:3:"ﳏ";s:4:"مح";s:3:"ﳐ";s:4:"مخ";s:3:"ﳑ";s:4:"مم";s:3:"ﳒ";s:4:"نج";s:3:"ﳓ";s:4:"نح";s:3:"ﳔ";s:4:"نخ";s:3:"ﳕ";s:4:"نم";s:3:"ﳖ";s:4:"نه";s:3:"ﳗ";s:4:"هج";s:3:"ﳘ";s:4:"هم";s:3:"ﳙ";s:4:"هٰ";s:3:"ﳚ";s:4:"يج";s:3:"ﳛ";s:4:"يح";s:3:"ﳜ";s:4:"يخ";s:3:"ﳝ";s:4:"يم";s:3:"ﳞ";s:4:"يه";s:3:"ﳟ";s:6:"ئم";s:3:"ﳠ";s:6:"ئه";s:3:"ﳡ";s:4:"بم";s:3:"ﳢ";s:4:"به";s:3:"ﳣ";s:4:"تم";s:3:"ﳤ";s:4:"ته";s:3:"ﳥ";s:4:"ثم";s:3:"ﳦ";s:4:"ثه";s:3:"ﳧ";s:4:"سم";s:3:"ﳨ";s:4:"سه";s:3:"ﳩ";s:4:"شم";s:3:"ﳪ";s:4:"شه";s:3:"ﳫ";s:4:"كل";s:3:"ﳬ";s:4:"كم";s:3:"ﳭ";s:4:"لم";s:3:"ﳮ";s:4:"نم";s:3:"ﳯ";s:4:"نه";s:3:"ﳰ";s:4:"يم";s:3:"ﳱ";s:4:"يه";s:3:"ﳲ";s:6:"ـَّ";s:3:"ﳳ";s:6:"ـُّ";s:3:"ﳴ";s:6:"ـِّ";s:3:"ﳵ";s:4:"طى";s:3:"ﳶ";s:4:"طي";s:3:"ﳷ";s:4:"عى";s:3:"ﳸ";s:4:"عي";s:3:"ﳹ";s:4:"غى";s:3:"ﳺ";s:4:"غي";s:3:"ﳻ";s:4:"سى";s:3:"ﳼ";s:4:"سي";s:3:"ﳽ";s:4:"شى";s:3:"ﳾ";s:4:"شي";s:3:"ﳿ";s:4:"حى";s:3:"ﴀ";s:4:"حي";s:3:"ﴁ";s:4:"جى";s:3:"ﴂ";s:4:"جي";s:3:"ﴃ";s:4:"خى";s:3:"ﴄ";s:4:"خي";s:3:"ﴅ";s:4:"صى";s:3:"ﴆ";s:4:"صي";s:3:"ﴇ";s:4:"ضى";s:3:"ﴈ";s:4:"ضي";s:3:"ﴉ";s:4:"شج";s:3:"ﴊ";s:4:"شح";s:3:"ﴋ";s:4:"شخ";s:3:"ﴌ";s:4:"شم";s:3:"ﴍ";s:4:"شر";s:3:"ﴎ";s:4:"سر";s:3:"ﴏ";s:4:"صر";s:3:"ﴐ";s:4:"ضر";s:3:"ﴑ";s:4:"طى";s:3:"ﴒ";s:4:"طي";s:3:"ﴓ";s:4:"عى";s:3:"ﴔ";s:4:"عي";s:3:"ﴕ";s:4:"غى";s:3:"ﴖ";s:4:"غي";s:3:"ﴗ";s:4:"سى";s:3:"ﴘ";s:4:"سي";s:3:"ﴙ";s:4:"شى";s:3:"ﴚ";s:4:"شي";s:3:"ﴛ";s:4:"حى";s:3:"ﴜ";s:4:"حي";s:3:"ﴝ";s:4:"جى";s:3:"ﴞ";s:4:"جي";s:3:"ﴟ";s:4:"خى";s:3:"ﴠ";s:4:"خي";s:3:"ﴡ";s:4:"صى";s:3:"ﴢ";s:4:"صي";s:3:"ﴣ";s:4:"ضى";s:3:"ﴤ";s:4:"ضي";s:3:"ﴥ";s:4:"شج";s:3:"ﴦ";s:4:"شح";s:3:"ﴧ";s:4:"شخ";s:3:"ﴨ";s:4:"شم";s:3:"ﴩ";s:4:"شر";s:3:"ﴪ";s:4:"سر";s:3:"ﴫ";s:4:"صر";s:3:"ﴬ";s:4:"ضر";s:3:"ﴭ";s:4:"شج";s:3:"ﴮ";s:4:"شح";s:3:"ﴯ";s:4:"شخ";s:3:"ﴰ";s:4:"شم";s:3:"ﴱ";s:4:"سه";s:3:"ﴲ";s:4:"شه";s:3:"ﴳ";s:4:"طم";s:3:"ﴴ";s:4:"سج";s:3:"ﴵ";s:4:"سح";s:3:"ﴶ";s:4:"سخ";s:3:"ﴷ";s:4:"شج";s:3:"ﴸ";s:4:"شح";s:3:"ﴹ";s:4:"شخ";s:3:"ﴺ";s:4:"طم";s:3:"ﴻ";s:4:"ظم";s:3:"ﴼ";s:4:"اً";s:3:"ﴽ";s:4:"اً";s:3:"ﵐ";s:6:"تجم";s:3:"ﵑ";s:6:"تحج";s:3:"ﵒ";s:6:"تحج";s:3:"ﵓ";s:6:"تحم";s:3:"ﵔ";s:6:"تخم";s:3:"ﵕ";s:6:"تمج";s:3:"ﵖ";s:6:"تمح";s:3:"ﵗ";s:6:"تمخ";s:3:"ﵘ";s:6:"جمح";s:3:"ﵙ";s:6:"جمح";s:3:"ﵚ";s:6:"حمي";s:3:"ﵛ";s:6:"حمى";s:3:"ﵜ";s:6:"سحج";s:3:"ﵝ";s:6:"سجح";s:3:"ﵞ";s:6:"سجى";s:3:"ﵟ";s:6:"سمح";s:3:"ﵠ";s:6:"سمح";s:3:"ﵡ";s:6:"سمج";s:3:"ﵢ";s:6:"سمم";s:3:"ﵣ";s:6:"سمم";s:3:"ﵤ";s:6:"صحح";s:3:"ﵥ";s:6:"صحح";s:3:"ﵦ";s:6:"صمم";s:3:"ﵧ";s:6:"شحم";s:3:"ﵨ";s:6:"شحم";s:3:"ﵩ";s:6:"شجي";s:3:"ﵪ";s:6:"شمخ";s:3:"ﵫ";s:6:"شمخ";s:3:"ﵬ";s:6:"شمم";s:3:"ﵭ";s:6:"شمم";s:3:"ﵮ";s:6:"ضحى";s:3:"ﵯ";s:6:"ضخم";s:3:"ﵰ";s:6:"ضخم";s:3:"ﵱ";s:6:"طمح";s:3:"ﵲ";s:6:"طمح";s:3:"ﵳ";s:6:"طمم";s:3:"ﵴ";s:6:"طمي";s:3:"ﵵ";s:6:"عجم";s:3:"ﵶ";s:6:"عمم";s:3:"ﵷ";s:6:"عمم";s:3:"ﵸ";s:6:"عمى";s:3:"ﵹ";s:6:"غمم";s:3:"ﵺ";s:6:"غمي";s:3:"ﵻ";s:6:"غمى";s:3:"ﵼ";s:6:"فخم";s:3:"ﵽ";s:6:"فخم";s:3:"ﵾ";s:6:"قمح";s:3:"ﵿ";s:6:"قمم";s:3:"ﶀ";s:6:"لحم";s:3:"ﶁ";s:6:"لحي";s:3:"ﶂ";s:6:"لحى";s:3:"ﶃ";s:6:"لجج";s:3:"ﶄ";s:6:"لجج";s:3:"ﶅ";s:6:"لخم";s:3:"ﶆ";s:6:"لخم";s:3:"ﶇ";s:6:"لمح";s:3:"ﶈ";s:6:"لمح";s:3:"ﶉ";s:6:"محج";s:3:"ﶊ";s:6:"محم";s:3:"ﶋ";s:6:"محي";s:3:"ﶌ";s:6:"مجح";s:3:"ﶍ";s:6:"مجم";s:3:"ﶎ";s:6:"مخج";s:3:"ﶏ";s:6:"مخم";s:3:"ﶒ";s:6:"مجخ";s:3:"ﶓ";s:6:"همج";s:3:"ﶔ";s:6:"همم";s:3:"ﶕ";s:6:"نحم";s:3:"ﶖ";s:6:"نحى";s:3:"ﶗ";s:6:"نجم";s:3:"ﶘ";s:6:"نجم";s:3:"ﶙ";s:6:"نجى";s:3:"ﶚ";s:6:"نمي";s:3:"ﶛ";s:6:"نمى";s:3:"ﶜ";s:6:"يمم";s:3:"ﶝ";s:6:"يمم";s:3:"ﶞ";s:6:"بخي";s:3:"ﶟ";s:6:"تجي";s:3:"ﶠ";s:6:"تجى";s:3:"ﶡ";s:6:"تخي";s:3:"ﶢ";s:6:"تخى";s:3:"ﶣ";s:6:"تمي";s:3:"ﶤ";s:6:"تمى";s:3:"ﶥ";s:6:"جمي";s:3:"ﶦ";s:6:"جحى";s:3:"ﶧ";s:6:"جمى";s:3:"ﶨ";s:6:"سخى";s:3:"ﶩ";s:6:"صحي";s:3:"ﶪ";s:6:"شحي";s:3:"ﶫ";s:6:"ضحي";s:3:"ﶬ";s:6:"لجي";s:3:"ﶭ";s:6:"لمي";s:3:"ﶮ";s:6:"يحي";s:3:"ﶯ";s:6:"يجي";s:3:"ﶰ";s:6:"يمي";s:3:"ﶱ";s:6:"ممي";s:3:"ﶲ";s:6:"قمي";s:3:"ﶳ";s:6:"نحي";s:3:"ﶴ";s:6:"قمح";s:3:"ﶵ";s:6:"لحم";s:3:"ﶶ";s:6:"عمي";s:3:"ﶷ";s:6:"كمي";s:3:"ﶸ";s:6:"نجح";s:3:"ﶹ";s:6:"مخي";s:3:"ﶺ";s:6:"لجم";s:3:"ﶻ";s:6:"كمم";s:3:"ﶼ";s:6:"لجم";s:3:"ﶽ";s:6:"نجح";s:3:"ﶾ";s:6:"جحي";s:3:"ﶿ";s:6:"حجي";s:3:"ﷀ";s:6:"مجي";s:3:"ﷁ";s:6:"فمي";s:3:"ﷂ";s:6:"بحي";s:3:"ﷃ";s:6:"كمم";s:3:"ﷄ";s:6:"عجم";s:3:"ﷅ";s:6:"صمم";s:3:"ﷆ";s:6:"سخي";s:3:"ﷇ";s:6:"نجي";s:3:"ﷰ";s:6:"صلے";s:3:"ﷱ";s:6:"قلے";s:3:"ﷲ";s:8:"الله";s:3:"ﷳ";s:8:"اكبر";s:3:"ﷴ";s:8:"محمد";s:3:"ﷵ";s:8:"صلعم";s:3:"ﷶ";s:8:"رسول";s:3:"ﷷ";s:8:"عليه";s:3:"ﷸ";s:8:"وسلم";s:3:"ﷹ";s:6:"صلى";s:3:"ﷺ";s:33:"صلى الله عليه وسلم";s:3:"ﷻ";s:15:"جل جلاله";s:3:"﷼";s:8:"ریال";s:3:"︐";s:1:",";s:3:"︑";s:3:"、";s:3:"︒";s:3:"。";s:3:"︓";s:1:":";s:3:"︔";s:1:";";s:3:"︕";s:1:"!";s:3:"︖";s:1:"?";s:3:"︗";s:3:"〖";s:3:"︘";s:3:"〗";s:3:"︙";s:3:"...";s:3:"︰";s:2:"..";s:3:"︱";s:3:"—";s:3:"︲";s:3:"–";s:3:"︳";s:1:"_";s:3:"︴";s:1:"_";s:3:"︵";s:1:"(";s:3:"︶";s:1:")";s:3:"︷";s:1:"{";s:3:"︸";s:1:"}";s:3:"︹";s:3:"〔";s:3:"︺";s:3:"〕";s:3:"︻";s:3:"【";s:3:"︼";s:3:"】";s:3:"︽";s:3:"《";s:3:"︾";s:3:"》";s:3:"︿";s:3:"〈";s:3:"﹀";s:3:"〉";s:3:"﹁";s:3:"「";s:3:"﹂";s:3:"」";s:3:"﹃";s:3:"『";s:3:"﹄";s:3:"』";s:3:"﹇";s:1:"[";s:3:"﹈";s:1:"]";s:3:"﹉";s:3:" ̅";s:3:"﹊";s:3:" ̅";s:3:"﹋";s:3:" ̅";s:3:"﹌";s:3:" ̅";s:3:"﹍";s:1:"_";s:3:"﹎";s:1:"_";s:3:"﹏";s:1:"_";s:3:"﹐";s:1:",";s:3:"﹑";s:3:"、";s:3:"﹒";s:1:".";s:3:"﹔";s:1:";";s:3:"﹕";s:1:":";s:3:"﹖";s:1:"?";s:3:"﹗";s:1:"!";s:3:"﹘";s:3:"—";s:3:"﹙";s:1:"(";s:3:"﹚";s:1:")";s:3:"﹛";s:1:"{";s:3:"﹜";s:1:"}";s:3:"﹝";s:3:"〔";s:3:"﹞";s:3:"〕";s:3:"﹟";s:1:"#";s:3:"﹠";s:1:"&";s:3:"﹡";s:1:"*";s:3:"﹢";s:1:"+";s:3:"﹣";s:1:"-";s:3:"﹤";s:1:"<";s:3:"﹥";s:1:">";s:3:"﹦";s:1:"=";s:3:"﹨";s:1:"\\";s:3:"﹩";s:1:"$";s:3:"﹪";s:1:"%";s:3:"﹫";s:1:"@";s:3:"ﹰ";s:3:" ً";s:3:"ﹱ";s:4:"ـً";s:3:"ﹲ";s:3:" ٌ";s:3:"ﹴ";s:3:" ٍ";s:3:"ﹶ";s:3:" َ";s:3:"ﹷ";s:4:"ـَ";s:3:"ﹸ";s:3:" ُ";s:3:"ﹹ";s:4:"ـُ";s:3:"ﹺ";s:3:" ِ";s:3:"ﹻ";s:4:"ـِ";s:3:"ﹼ";s:3:" ّ";s:3:"ﹽ";s:4:"ـّ";s:3:"ﹾ";s:3:" ْ";s:3:"ﹿ";s:4:"ـْ";s:3:"ﺀ";s:2:"ء";s:3:"ﺁ";s:4:"آ";s:3:"ﺂ";s:4:"آ";s:3:"ﺃ";s:4:"أ";s:3:"ﺄ";s:4:"أ";s:3:"ﺅ";s:4:"ؤ";s:3:"ﺆ";s:4:"ؤ";s:3:"ﺇ";s:4:"إ";s:3:"ﺈ";s:4:"إ";s:3:"ﺉ";s:4:"ئ";s:3:"ﺊ";s:4:"ئ";s:3:"ﺋ";s:4:"ئ";s:3:"ﺌ";s:4:"ئ";s:3:"ﺍ";s:2:"ا";s:3:"ﺎ";s:2:"ا";s:3:"ﺏ";s:2:"ب";s:3:"ﺐ";s:2:"ب";s:3:"ﺑ";s:2:"ب";s:3:"ﺒ";s:2:"ب";s:3:"ﺓ";s:2:"ة";s:3:"ﺔ";s:2:"ة";s:3:"ﺕ";s:2:"ت";s:3:"ﺖ";s:2:"ت";s:3:"ﺗ";s:2:"ت";s:3:"ﺘ";s:2:"ت";s:3:"ﺙ";s:2:"ث";s:3:"ﺚ";s:2:"ث";s:3:"ﺛ";s:2:"ث";s:3:"ﺜ";s:2:"ث";s:3:"ﺝ";s:2:"ج";s:3:"ﺞ";s:2:"ج";s:3:"ﺟ";s:2:"ج";s:3:"ﺠ";s:2:"ج";s:3:"ﺡ";s:2:"ح";s:3:"ﺢ";s:2:"ح";s:3:"ﺣ";s:2:"ح";s:3:"ﺤ";s:2:"ح";s:3:"ﺥ";s:2:"خ";s:3:"ﺦ";s:2:"خ";s:3:"ﺧ";s:2:"خ";s:3:"ﺨ";s:2:"خ";s:3:"ﺩ";s:2:"د";s:3:"ﺪ";s:2:"د";s:3:"ﺫ";s:2:"ذ";s:3:"ﺬ";s:2:"ذ";s:3:"ﺭ";s:2:"ر";s:3:"ﺮ";s:2:"ر";s:3:"ﺯ";s:2:"ز";s:3:"ﺰ";s:2:"ز";s:3:"ﺱ";s:2:"س";s:3:"ﺲ";s:2:"س";s:3:"ﺳ";s:2:"س";s:3:"ﺴ";s:2:"س";s:3:"ﺵ";s:2:"ش";s:3:"ﺶ";s:2:"ش";s:3:"ﺷ";s:2:"ش";s:3:"ﺸ";s:2:"ش";s:3:"ﺹ";s:2:"ص";s:3:"ﺺ";s:2:"ص";s:3:"ﺻ";s:2:"ص";s:3:"ﺼ";s:2:"ص";s:3:"ﺽ";s:2:"ض";s:3:"ﺾ";s:2:"ض";s:3:"ﺿ";s:2:"ض";s:3:"ﻀ";s:2:"ض";s:3:"ﻁ";s:2:"ط";s:3:"ﻂ";s:2:"ط";s:3:"ﻃ";s:2:"ط";s:3:"ﻄ";s:2:"ط";s:3:"ﻅ";s:2:"ظ";s:3:"ﻆ";s:2:"ظ";s:3:"ﻇ";s:2:"ظ";s:3:"ﻈ";s:2:"ظ";s:3:"ﻉ";s:2:"ع";s:3:"ﻊ";s:2:"ع";s:3:"ﻋ";s:2:"ع";s:3:"ﻌ";s:2:"ع";s:3:"ﻍ";s:2:"غ";s:3:"ﻎ";s:2:"غ";s:3:"ﻏ";s:2:"غ";s:3:"ﻐ";s:2:"غ";s:3:"ﻑ";s:2:"ف";s:3:"ﻒ";s:2:"ف";s:3:"ﻓ";s:2:"ف";s:3:"ﻔ";s:2:"ف";s:3:"ﻕ";s:2:"ق";s:3:"ﻖ";s:2:"ق";s:3:"ﻗ";s:2:"ق";s:3:"ﻘ";s:2:"ق";s:3:"ﻙ";s:2:"ك";s:3:"ﻚ";s:2:"ك";s:3:"ﻛ";s:2:"ك";s:3:"ﻜ";s:2:"ك";s:3:"ﻝ";s:2:"ل";s:3:"ﻞ";s:2:"ل";s:3:"ﻟ";s:2:"ل";s:3:"ﻠ";s:2:"ل";s:3:"ﻡ";s:2:"م";s:3:"ﻢ";s:2:"م";s:3:"ﻣ";s:2:"م";s:3:"ﻤ";s:2:"م";s:3:"ﻥ";s:2:"ن";s:3:"ﻦ";s:2:"ن";s:3:"ﻧ";s:2:"ن";s:3:"ﻨ";s:2:"ن";s:3:"ﻩ";s:2:"ه";s:3:"ﻪ";s:2:"ه";s:3:"ﻫ";s:2:"ه";s:3:"ﻬ";s:2:"ه";s:3:"ﻭ";s:2:"و";s:3:"ﻮ";s:2:"و";s:3:"ﻯ";s:2:"ى";s:3:"ﻰ";s:2:"ى";s:3:"ﻱ";s:2:"ي";s:3:"ﻲ";s:2:"ي";s:3:"ﻳ";s:2:"ي";s:3:"ﻴ";s:2:"ي";s:3:"ﻵ";s:6:"لآ";s:3:"ﻶ";s:6:"لآ";s:3:"ﻷ";s:6:"لأ";s:3:"ﻸ";s:6:"لأ";s:3:"ﻹ";s:6:"لإ";s:3:"ﻺ";s:6:"لإ";s:3:"ﻻ";s:4:"لا";s:3:"ﻼ";s:4:"لا";s:3:"!";s:1:"!";s:3:""";s:1:""";s:3:"#";s:1:"#";s:3:"$";s:1:"$";s:3:"%";s:1:"%";s:3:"&";s:1:"&";s:3:"'";s:1:"\'";s:3:"(";s:1:"(";s:3:")";s:1:")";s:3:"*";s:1:"*";s:3:"+";s:1:"+";s:3:",";s:1:",";s:3:"-";s:1:"-";s:3:".";s:1:".";s:3:"/";s:1:"/";s:3:"0";s:1:"0";s:3:"1";s:1:"1";s:3:"2";s:1:"2";s:3:"3";s:1:"3";s:3:"4";s:1:"4";s:3:"5";s:1:"5";s:3:"6";s:1:"6";s:3:"7";s:1:"7";s:3:"8";s:1:"8";s:3:"9";s:1:"9";s:3:":";s:1:":";s:3:";";s:1:";";s:3:"<";s:1:"<";s:3:"=";s:1:"=";s:3:">";s:1:">";s:3:"?";s:1:"?";s:3:"@";s:1:"@";s:3:"A";s:1:"A";s:3:"B";s:1:"B";s:3:"C";s:1:"C";s:3:"D";s:1:"D";s:3:"E";s:1:"E";s:3:"F";s:1:"F";s:3:"G";s:1:"G";s:3:"H";s:1:"H";s:3:"I";s:1:"I";s:3:"J";s:1:"J";s:3:"K";s:1:"K";s:3:"L";s:1:"L";s:3:"M";s:1:"M";s:3:"N";s:1:"N";s:3:"O";s:1:"O";s:3:"P";s:1:"P";s:3:"Q";s:1:"Q";s:3:"R";s:1:"R";s:3:"S";s:1:"S";s:3:"T";s:1:"T";s:3:"U";s:1:"U";s:3:"V";s:1:"V";s:3:"W";s:1:"W";s:3:"X";s:1:"X";s:3:"Y";s:1:"Y";s:3:"Z";s:1:"Z";s:3:"[";s:1:"[";s:3:"\";s:1:"\\";s:3:"]";s:1:"]";s:3:"^";s:1:"^";s:3:"_";s:1:"_";s:3:"`";s:1:"`";s:3:"a";s:1:"a";s:3:"b";s:1:"b";s:3:"c";s:1:"c";s:3:"d";s:1:"d";s:3:"e";s:1:"e";s:3:"f";s:1:"f";s:3:"g";s:1:"g";s:3:"h";s:1:"h";s:3:"i";s:1:"i";s:3:"j";s:1:"j";s:3:"k";s:1:"k";s:3:"l";s:1:"l";s:3:"m";s:1:"m";s:3:"n";s:1:"n";s:3:"o";s:1:"o";s:3:"p";s:1:"p";s:3:"q";s:1:"q";s:3:"r";s:1:"r";s:3:"s";s:1:"s";s:3:"t";s:1:"t";s:3:"u";s:1:"u";s:3:"v";s:1:"v";s:3:"w";s:1:"w";s:3:"x";s:1:"x";s:3:"y";s:1:"y";s:3:"z";s:1:"z";s:3:"{";s:1:"{";s:3:"|";s:1:"|";s:3:"}";s:1:"}";s:3:"~";s:1:"~";s:3:"⦅";s:3:"⦅";s:3:"⦆";s:3:"⦆";s:3:"。";s:3:"。";s:3:"「";s:3:"「";s:3:"」";s:3:"」";s:3:"、";s:3:"、";s:3:"・";s:3:"・";s:3:"ヲ";s:3:"ヲ";s:3:"ァ";s:3:"ァ";s:3:"ィ";s:3:"ィ";s:3:"ゥ";s:3:"ゥ";s:3:"ェ";s:3:"ェ";s:3:"ォ";s:3:"ォ";s:3:"ャ";s:3:"ャ";s:3:"ュ";s:3:"ュ";s:3:"ョ";s:3:"ョ";s:3:"ッ";s:3:"ッ";s:3:"ー";s:3:"ー";s:3:"ア";s:3:"ア";s:3:"イ";s:3:"イ";s:3:"ウ";s:3:"ウ";s:3:"エ";s:3:"エ";s:3:"オ";s:3:"オ";s:3:"カ";s:3:"カ";s:3:"キ";s:3:"キ";s:3:"ク";s:3:"ク";s:3:"ケ";s:3:"ケ";s:3:"コ";s:3:"コ";s:3:"サ";s:3:"サ";s:3:"シ";s:3:"シ";s:3:"ス";s:3:"ス";s:3:"セ";s:3:"セ";s:3:"ソ";s:3:"ソ";s:3:"タ";s:3:"タ";s:3:"チ";s:3:"チ";s:3:"ツ";s:3:"ツ";s:3:"テ";s:3:"テ";s:3:"ト";s:3:"ト";s:3:"ナ";s:3:"ナ";s:3:"ニ";s:3:"ニ";s:3:"ヌ";s:3:"ヌ";s:3:"ネ";s:3:"ネ";s:3:"ノ";s:3:"ノ";s:3:"ハ";s:3:"ハ";s:3:"ヒ";s:3:"ヒ";s:3:"フ";s:3:"フ";s:3:"ヘ";s:3:"ヘ";s:3:"ホ";s:3:"ホ";s:3:"マ";s:3:"マ";s:3:"ミ";s:3:"ミ";s:3:"ム";s:3:"ム";s:3:"メ";s:3:"メ";s:3:"モ";s:3:"モ";s:3:"ヤ";s:3:"ヤ";s:3:"ユ";s:3:"ユ";s:3:"ヨ";s:3:"ヨ";s:3:"ラ";s:3:"ラ";s:3:"リ";s:3:"リ";s:3:"ル";s:3:"ル";s:3:"レ";s:3:"レ";s:3:"ロ";s:3:"ロ";s:3:"ワ";s:3:"ワ";s:3:"ン";s:3:"ン";s:3:"゙";s:3:"゙";s:3:"゚";s:3:"゚";s:3:"ᅠ";s:3:"ᅠ";s:3:"ᄀ";s:3:"ᄀ";s:3:"ᄁ";s:3:"ᄁ";s:3:"ᆪ";s:3:"ᆪ";s:3:"ᄂ";s:3:"ᄂ";s:3:"ᆬ";s:3:"ᆬ";s:3:"ᆭ";s:3:"ᆭ";s:3:"ᄃ";s:3:"ᄃ";s:3:"ᄄ";s:3:"ᄄ";s:3:"ᄅ";s:3:"ᄅ";s:3:"ᆰ";s:3:"ᆰ";s:3:"ᆱ";s:3:"ᆱ";s:3:"ᆲ";s:3:"ᆲ";s:3:"ᆳ";s:3:"ᆳ";s:3:"ᆴ";s:3:"ᆴ";s:3:"ᆵ";s:3:"ᆵ";s:3:"ᄚ";s:3:"ᄚ";s:3:"ᄆ";s:3:"ᄆ";s:3:"ᄇ";s:3:"ᄇ";s:3:"ᄈ";s:3:"ᄈ";s:3:"ᄡ";s:3:"ᄡ";s:3:"ᄉ";s:3:"ᄉ";s:3:"ᄊ";s:3:"ᄊ";s:3:"ᄋ";s:3:"ᄋ";s:3:"ᄌ";s:3:"ᄌ";s:3:"ᄍ";s:3:"ᄍ";s:3:"ᄎ";s:3:"ᄎ";s:3:"ᄏ";s:3:"ᄏ";s:3:"ᄐ";s:3:"ᄐ";s:3:"ᄑ";s:3:"ᄑ";s:3:"ᄒ";s:3:"ᄒ";s:3:"ᅡ";s:3:"ᅡ";s:3:"ᅢ";s:3:"ᅢ";s:3:"ᅣ";s:3:"ᅣ";s:3:"ᅤ";s:3:"ᅤ";s:3:"ᅥ";s:3:"ᅥ";s:3:"ᅦ";s:3:"ᅦ";s:3:"ᅧ";s:3:"ᅧ";s:3:"ᅨ";s:3:"ᅨ";s:3:"ᅩ";s:3:"ᅩ";s:3:"ᅪ";s:3:"ᅪ";s:3:"ᅫ";s:3:"ᅫ";s:3:"ᅬ";s:3:"ᅬ";s:3:"ᅭ";s:3:"ᅭ";s:3:"ᅮ";s:3:"ᅮ";s:3:"ᅯ";s:3:"ᅯ";s:3:"ᅰ";s:3:"ᅰ";s:3:"ᅱ";s:3:"ᅱ";s:3:"ᅲ";s:3:"ᅲ";s:3:"ᅳ";s:3:"ᅳ";s:3:"ᅴ";s:3:"ᅴ";s:3:"ᅵ";s:3:"ᅵ";s:3:"¢";s:2:"¢";s:3:"£";s:2:"£";s:3:"¬";s:2:"¬";s:3:" ̄";s:3:" ̄";s:3:"¦";s:2:"¦";s:3:"¥";s:2:"¥";s:3:"₩";s:3:"₩";s:3:"│";s:3:"│";s:3:"←";s:3:"←";s:3:"↑";s:3:"↑";s:3:"→";s:3:"→";s:3:"↓";s:3:"↓";s:3:"■";s:3:"■";s:3:"○";s:3:"○";s:4:"𝅗𝅥";s:8:"𝅗𝅥";s:4:"𝅘𝅥";s:8:"𝅘𝅥";s:4:"𝅘𝅥𝅮";s:12:"𝅘𝅥𝅮";s:4:"𝅘𝅥𝅯";s:12:"𝅘𝅥𝅯";s:4:"𝅘𝅥𝅰";s:12:"𝅘𝅥𝅰";s:4:"𝅘𝅥𝅱";s:12:"𝅘𝅥𝅱";s:4:"𝅘𝅥𝅲";s:12:"𝅘𝅥𝅲";s:4:"𝆹𝅥";s:8:"𝆹𝅥";s:4:"𝆺𝅥";s:8:"𝆺𝅥";s:4:"𝆹𝅥𝅮";s:12:"𝆹𝅥𝅮";s:4:"𝆺𝅥𝅮";s:12:"𝆺𝅥𝅮";s:4:"𝆹𝅥𝅯";s:12:"𝆹𝅥𝅯";s:4:"𝆺𝅥𝅯";s:12:"𝆺𝅥𝅯";s:4:"𝐀";s:1:"A";s:4:"𝐁";s:1:"B";s:4:"𝐂";s:1:"C";s:4:"𝐃";s:1:"D";s:4:"𝐄";s:1:"E";s:4:"𝐅";s:1:"F";s:4:"𝐆";s:1:"G";s:4:"𝐇";s:1:"H";s:4:"𝐈";s:1:"I";s:4:"𝐉";s:1:"J";s:4:"𝐊";s:1:"K";s:4:"𝐋";s:1:"L";s:4:"𝐌";s:1:"M";s:4:"𝐍";s:1:"N";s:4:"𝐎";s:1:"O";s:4:"𝐏";s:1:"P";s:4:"𝐐";s:1:"Q";s:4:"𝐑";s:1:"R";s:4:"𝐒";s:1:"S";s:4:"𝐓";s:1:"T";s:4:"𝐔";s:1:"U";s:4:"𝐕";s:1:"V";s:4:"𝐖";s:1:"W";s:4:"𝐗";s:1:"X";s:4:"𝐘";s:1:"Y";s:4:"𝐙";s:1:"Z";s:4:"𝐚";s:1:"a";s:4:"𝐛";s:1:"b";s:4:"𝐜";s:1:"c";s:4:"𝐝";s:1:"d";s:4:"𝐞";s:1:"e";s:4:"𝐟";s:1:"f";s:4:"𝐠";s:1:"g";s:4:"𝐡";s:1:"h";s:4:"𝐢";s:1:"i";s:4:"𝐣";s:1:"j";s:4:"𝐤";s:1:"k";s:4:"𝐥";s:1:"l";s:4:"𝐦";s:1:"m";s:4:"𝐧";s:1:"n";s:4:"𝐨";s:1:"o";s:4:"𝐩";s:1:"p";s:4:"𝐪";s:1:"q";s:4:"𝐫";s:1:"r";s:4:"𝐬";s:1:"s";s:4:"𝐭";s:1:"t";s:4:"𝐮";s:1:"u";s:4:"𝐯";s:1:"v";s:4:"𝐰";s:1:"w";s:4:"𝐱";s:1:"x";s:4:"𝐲";s:1:"y";s:4:"𝐳";s:1:"z";s:4:"𝐴";s:1:"A";s:4:"𝐵";s:1:"B";s:4:"𝐶";s:1:"C";s:4:"𝐷";s:1:"D";s:4:"𝐸";s:1:"E";s:4:"𝐹";s:1:"F";s:4:"𝐺";s:1:"G";s:4:"𝐻";s:1:"H";s:4:"𝐼";s:1:"I";s:4:"𝐽";s:1:"J";s:4:"𝐾";s:1:"K";s:4:"𝐿";s:1:"L";s:4:"𝑀";s:1:"M";s:4:"𝑁";s:1:"N";s:4:"𝑂";s:1:"O";s:4:"𝑃";s:1:"P";s:4:"𝑄";s:1:"Q";s:4:"𝑅";s:1:"R";s:4:"𝑆";s:1:"S";s:4:"𝑇";s:1:"T";s:4:"𝑈";s:1:"U";s:4:"𝑉";s:1:"V";s:4:"𝑊";s:1:"W";s:4:"𝑋";s:1:"X";s:4:"𝑌";s:1:"Y";s:4:"𝑍";s:1:"Z";s:4:"𝑎";s:1:"a";s:4:"𝑏";s:1:"b";s:4:"𝑐";s:1:"c";s:4:"𝑑";s:1:"d";s:4:"𝑒";s:1:"e";s:4:"𝑓";s:1:"f";s:4:"𝑔";s:1:"g";s:4:"𝑖";s:1:"i";s:4:"𝑗";s:1:"j";s:4:"𝑘";s:1:"k";s:4:"𝑙";s:1:"l";s:4:"𝑚";s:1:"m";s:4:"𝑛";s:1:"n";s:4:"𝑜";s:1:"o";s:4:"𝑝";s:1:"p";s:4:"𝑞";s:1:"q";s:4:"𝑟";s:1:"r";s:4:"𝑠";s:1:"s";s:4:"𝑡";s:1:"t";s:4:"𝑢";s:1:"u";s:4:"𝑣";s:1:"v";s:4:"𝑤";s:1:"w";s:4:"𝑥";s:1:"x";s:4:"𝑦";s:1:"y";s:4:"𝑧";s:1:"z";s:4:"𝑨";s:1:"A";s:4:"𝑩";s:1:"B";s:4:"𝑪";s:1:"C";s:4:"𝑫";s:1:"D";s:4:"𝑬";s:1:"E";s:4:"𝑭";s:1:"F";s:4:"𝑮";s:1:"G";s:4:"𝑯";s:1:"H";s:4:"𝑰";s:1:"I";s:4:"𝑱";s:1:"J";s:4:"𝑲";s:1:"K";s:4:"𝑳";s:1:"L";s:4:"𝑴";s:1:"M";s:4:"𝑵";s:1:"N";s:4:"𝑶";s:1:"O";s:4:"𝑷";s:1:"P";s:4:"𝑸";s:1:"Q";s:4:"𝑹";s:1:"R";s:4:"𝑺";s:1:"S";s:4:"𝑻";s:1:"T";s:4:"𝑼";s:1:"U";s:4:"𝑽";s:1:"V";s:4:"𝑾";s:1:"W";s:4:"𝑿";s:1:"X";s:4:"𝒀";s:1:"Y";s:4:"𝒁";s:1:"Z";s:4:"𝒂";s:1:"a";s:4:"𝒃";s:1:"b";s:4:"𝒄";s:1:"c";s:4:"𝒅";s:1:"d";s:4:"𝒆";s:1:"e";s:4:"𝒇";s:1:"f";s:4:"𝒈";s:1:"g";s:4:"𝒉";s:1:"h";s:4:"𝒊";s:1:"i";s:4:"𝒋";s:1:"j";s:4:"𝒌";s:1:"k";s:4:"𝒍";s:1:"l";s:4:"𝒎";s:1:"m";s:4:"𝒏";s:1:"n";s:4:"𝒐";s:1:"o";s:4:"𝒑";s:1:"p";s:4:"𝒒";s:1:"q";s:4:"𝒓";s:1:"r";s:4:"𝒔";s:1:"s";s:4:"𝒕";s:1:"t";s:4:"𝒖";s:1:"u";s:4:"𝒗";s:1:"v";s:4:"𝒘";s:1:"w";s:4:"𝒙";s:1:"x";s:4:"𝒚";s:1:"y";s:4:"𝒛";s:1:"z";s:4:"𝒜";s:1:"A";s:4:"𝒞";s:1:"C";s:4:"𝒟";s:1:"D";s:4:"𝒢";s:1:"G";s:4:"𝒥";s:1:"J";s:4:"𝒦";s:1:"K";s:4:"𝒩";s:1:"N";s:4:"𝒪";s:1:"O";s:4:"𝒫";s:1:"P";s:4:"𝒬";s:1:"Q";s:4:"𝒮";s:1:"S";s:4:"𝒯";s:1:"T";s:4:"𝒰";s:1:"U";s:4:"𝒱";s:1:"V";s:4:"𝒲";s:1:"W";s:4:"𝒳";s:1:"X";s:4:"𝒴";s:1:"Y";s:4:"𝒵";s:1:"Z";s:4:"𝒶";s:1:"a";s:4:"𝒷";s:1:"b";s:4:"𝒸";s:1:"c";s:4:"𝒹";s:1:"d";s:4:"𝒻";s:1:"f";s:4:"𝒽";s:1:"h";s:4:"𝒾";s:1:"i";s:4:"𝒿";s:1:"j";s:4:"𝓀";s:1:"k";s:4:"𝓁";s:1:"l";s:4:"𝓂";s:1:"m";s:4:"𝓃";s:1:"n";s:4:"𝓅";s:1:"p";s:4:"𝓆";s:1:"q";s:4:"𝓇";s:1:"r";s:4:"𝓈";s:1:"s";s:4:"𝓉";s:1:"t";s:4:"𝓊";s:1:"u";s:4:"𝓋";s:1:"v";s:4:"𝓌";s:1:"w";s:4:"𝓍";s:1:"x";s:4:"𝓎";s:1:"y";s:4:"𝓏";s:1:"z";s:4:"𝓐";s:1:"A";s:4:"𝓑";s:1:"B";s:4:"𝓒";s:1:"C";s:4:"𝓓";s:1:"D";s:4:"𝓔";s:1:"E";s:4:"𝓕";s:1:"F";s:4:"𝓖";s:1:"G";s:4:"𝓗";s:1:"H";s:4:"𝓘";s:1:"I";s:4:"𝓙";s:1:"J";s:4:"𝓚";s:1:"K";s:4:"𝓛";s:1:"L";s:4:"𝓜";s:1:"M";s:4:"𝓝";s:1:"N";s:4:"𝓞";s:1:"O";s:4:"𝓟";s:1:"P";s:4:"𝓠";s:1:"Q";s:4:"𝓡";s:1:"R";s:4:"𝓢";s:1:"S";s:4:"𝓣";s:1:"T";s:4:"𝓤";s:1:"U";s:4:"𝓥";s:1:"V";s:4:"𝓦";s:1:"W";s:4:"𝓧";s:1:"X";s:4:"𝓨";s:1:"Y";s:4:"𝓩";s:1:"Z";s:4:"𝓪";s:1:"a";s:4:"𝓫";s:1:"b";s:4:"𝓬";s:1:"c";s:4:"𝓭";s:1:"d";s:4:"𝓮";s:1:"e";s:4:"𝓯";s:1:"f";s:4:"𝓰";s:1:"g";s:4:"𝓱";s:1:"h";s:4:"𝓲";s:1:"i";s:4:"𝓳";s:1:"j";s:4:"𝓴";s:1:"k";s:4:"𝓵";s:1:"l";s:4:"𝓶";s:1:"m";s:4:"𝓷";s:1:"n";s:4:"𝓸";s:1:"o";s:4:"𝓹";s:1:"p";s:4:"𝓺";s:1:"q";s:4:"𝓻";s:1:"r";s:4:"𝓼";s:1:"s";s:4:"𝓽";s:1:"t";s:4:"𝓾";s:1:"u";s:4:"𝓿";s:1:"v";s:4:"𝔀";s:1:"w";s:4:"𝔁";s:1:"x";s:4:"𝔂";s:1:"y";s:4:"𝔃";s:1:"z";s:4:"𝔄";s:1:"A";s:4:"𝔅";s:1:"B";s:4:"𝔇";s:1:"D";s:4:"𝔈";s:1:"E";s:4:"𝔉";s:1:"F";s:4:"𝔊";s:1:"G";s:4:"𝔍";s:1:"J";s:4:"𝔎";s:1:"K";s:4:"𝔏";s:1:"L";s:4:"𝔐";s:1:"M";s:4:"𝔑";s:1:"N";s:4:"𝔒";s:1:"O";s:4:"𝔓";s:1:"P";s:4:"𝔔";s:1:"Q";s:4:"𝔖";s:1:"S";s:4:"𝔗";s:1:"T";s:4:"𝔘";s:1:"U";s:4:"𝔙";s:1:"V";s:4:"𝔚";s:1:"W";s:4:"𝔛";s:1:"X";s:4:"𝔜";s:1:"Y";s:4:"𝔞";s:1:"a";s:4:"𝔟";s:1:"b";s:4:"𝔠";s:1:"c";s:4:"𝔡";s:1:"d";s:4:"𝔢";s:1:"e";s:4:"𝔣";s:1:"f";s:4:"𝔤";s:1:"g";s:4:"𝔥";s:1:"h";s:4:"𝔦";s:1:"i";s:4:"𝔧";s:1:"j";s:4:"𝔨";s:1:"k";s:4:"𝔩";s:1:"l";s:4:"𝔪";s:1:"m";s:4:"𝔫";s:1:"n";s:4:"𝔬";s:1:"o";s:4:"𝔭";s:1:"p";s:4:"𝔮";s:1:"q";s:4:"𝔯";s:1:"r";s:4:"𝔰";s:1:"s";s:4:"𝔱";s:1:"t";s:4:"𝔲";s:1:"u";s:4:"𝔳";s:1:"v";s:4:"𝔴";s:1:"w";s:4:"𝔵";s:1:"x";s:4:"𝔶";s:1:"y";s:4:"𝔷";s:1:"z";s:4:"𝔸";s:1:"A";s:4:"𝔹";s:1:"B";s:4:"𝔻";s:1:"D";s:4:"𝔼";s:1:"E";s:4:"𝔽";s:1:"F";s:4:"𝔾";s:1:"G";s:4:"𝕀";s:1:"I";s:4:"𝕁";s:1:"J";s:4:"𝕂";s:1:"K";s:4:"𝕃";s:1:"L";s:4:"𝕄";s:1:"M";s:4:"𝕆";s:1:"O";s:4:"𝕊";s:1:"S";s:4:"𝕋";s:1:"T";s:4:"𝕌";s:1:"U";s:4:"𝕍";s:1:"V";s:4:"𝕎";s:1:"W";s:4:"𝕏";s:1:"X";s:4:"𝕐";s:1:"Y";s:4:"𝕒";s:1:"a";s:4:"𝕓";s:1:"b";s:4:"𝕔";s:1:"c";s:4:"𝕕";s:1:"d";s:4:"𝕖";s:1:"e";s:4:"𝕗";s:1:"f";s:4:"𝕘";s:1:"g";s:4:"𝕙";s:1:"h";s:4:"𝕚";s:1:"i";s:4:"𝕛";s:1:"j";s:4:"𝕜";s:1:"k";s:4:"𝕝";s:1:"l";s:4:"𝕞";s:1:"m";s:4:"𝕟";s:1:"n";s:4:"𝕠";s:1:"o";s:4:"𝕡";s:1:"p";s:4:"𝕢";s:1:"q";s:4:"𝕣";s:1:"r";s:4:"𝕤";s:1:"s";s:4:"𝕥";s:1:"t";s:4:"𝕦";s:1:"u";s:4:"𝕧";s:1:"v";s:4:"𝕨";s:1:"w";s:4:"𝕩";s:1:"x";s:4:"𝕪";s:1:"y";s:4:"𝕫";s:1:"z";s:4:"𝕬";s:1:"A";s:4:"𝕭";s:1:"B";s:4:"𝕮";s:1:"C";s:4:"𝕯";s:1:"D";s:4:"𝕰";s:1:"E";s:4:"𝕱";s:1:"F";s:4:"𝕲";s:1:"G";s:4:"𝕳";s:1:"H";s:4:"𝕴";s:1:"I";s:4:"𝕵";s:1:"J";s:4:"𝕶";s:1:"K";s:4:"𝕷";s:1:"L";s:4:"𝕸";s:1:"M";s:4:"𝕹";s:1:"N";s:4:"𝕺";s:1:"O";s:4:"𝕻";s:1:"P";s:4:"𝕼";s:1:"Q";s:4:"𝕽";s:1:"R";s:4:"𝕾";s:1:"S";s:4:"𝕿";s:1:"T";s:4:"𝖀";s:1:"U";s:4:"𝖁";s:1:"V";s:4:"𝖂";s:1:"W";s:4:"𝖃";s:1:"X";s:4:"𝖄";s:1:"Y";s:4:"𝖅";s:1:"Z";s:4:"𝖆";s:1:"a";s:4:"𝖇";s:1:"b";s:4:"𝖈";s:1:"c";s:4:"𝖉";s:1:"d";s:4:"𝖊";s:1:"e";s:4:"𝖋";s:1:"f";s:4:"𝖌";s:1:"g";s:4:"𝖍";s:1:"h";s:4:"𝖎";s:1:"i";s:4:"𝖏";s:1:"j";s:4:"𝖐";s:1:"k";s:4:"𝖑";s:1:"l";s:4:"𝖒";s:1:"m";s:4:"𝖓";s:1:"n";s:4:"𝖔";s:1:"o";s:4:"𝖕";s:1:"p";s:4:"𝖖";s:1:"q";s:4:"𝖗";s:1:"r";s:4:"𝖘";s:1:"s";s:4:"𝖙";s:1:"t";s:4:"𝖚";s:1:"u";s:4:"𝖛";s:1:"v";s:4:"𝖜";s:1:"w";s:4:"𝖝";s:1:"x";s:4:"𝖞";s:1:"y";s:4:"𝖟";s:1:"z";s:4:"𝖠";s:1:"A";s:4:"𝖡";s:1:"B";s:4:"𝖢";s:1:"C";s:4:"𝖣";s:1:"D";s:4:"𝖤";s:1:"E";s:4:"𝖥";s:1:"F";s:4:"𝖦";s:1:"G";s:4:"𝖧";s:1:"H";s:4:"𝖨";s:1:"I";s:4:"𝖩";s:1:"J";s:4:"𝖪";s:1:"K";s:4:"𝖫";s:1:"L";s:4:"𝖬";s:1:"M";s:4:"𝖭";s:1:"N";s:4:"𝖮";s:1:"O";s:4:"𝖯";s:1:"P";s:4:"𝖰";s:1:"Q";s:4:"𝖱";s:1:"R";s:4:"𝖲";s:1:"S";s:4:"𝖳";s:1:"T";s:4:"𝖴";s:1:"U";s:4:"𝖵";s:1:"V";s:4:"𝖶";s:1:"W";s:4:"𝖷";s:1:"X";s:4:"𝖸";s:1:"Y";s:4:"𝖹";s:1:"Z";s:4:"𝖺";s:1:"a";s:4:"𝖻";s:1:"b";s:4:"𝖼";s:1:"c";s:4:"𝖽";s:1:"d";s:4:"𝖾";s:1:"e";s:4:"𝖿";s:1:"f";s:4:"𝗀";s:1:"g";s:4:"𝗁";s:1:"h";s:4:"𝗂";s:1:"i";s:4:"𝗃";s:1:"j";s:4:"𝗄";s:1:"k";s:4:"𝗅";s:1:"l";s:4:"𝗆";s:1:"m";s:4:"𝗇";s:1:"n";s:4:"𝗈";s:1:"o";s:4:"𝗉";s:1:"p";s:4:"𝗊";s:1:"q";s:4:"𝗋";s:1:"r";s:4:"𝗌";s:1:"s";s:4:"𝗍";s:1:"t";s:4:"𝗎";s:1:"u";s:4:"𝗏";s:1:"v";s:4:"𝗐";s:1:"w";s:4:"𝗑";s:1:"x";s:4:"𝗒";s:1:"y";s:4:"𝗓";s:1:"z";s:4:"𝗔";s:1:"A";s:4:"𝗕";s:1:"B";s:4:"𝗖";s:1:"C";s:4:"𝗗";s:1:"D";s:4:"𝗘";s:1:"E";s:4:"𝗙";s:1:"F";s:4:"𝗚";s:1:"G";s:4:"𝗛";s:1:"H";s:4:"𝗜";s:1:"I";s:4:"𝗝";s:1:"J";s:4:"𝗞";s:1:"K";s:4:"𝗟";s:1:"L";s:4:"𝗠";s:1:"M";s:4:"𝗡";s:1:"N";s:4:"𝗢";s:1:"O";s:4:"𝗣";s:1:"P";s:4:"𝗤";s:1:"Q";s:4:"𝗥";s:1:"R";s:4:"𝗦";s:1:"S";s:4:"𝗧";s:1:"T";s:4:"𝗨";s:1:"U";s:4:"𝗩";s:1:"V";s:4:"𝗪";s:1:"W";s:4:"𝗫";s:1:"X";s:4:"𝗬";s:1:"Y";s:4:"𝗭";s:1:"Z";s:4:"𝗮";s:1:"a";s:4:"𝗯";s:1:"b";s:4:"𝗰";s:1:"c";s:4:"𝗱";s:1:"d";s:4:"𝗲";s:1:"e";s:4:"𝗳";s:1:"f";s:4:"𝗴";s:1:"g";s:4:"𝗵";s:1:"h";s:4:"𝗶";s:1:"i";s:4:"𝗷";s:1:"j";s:4:"𝗸";s:1:"k";s:4:"𝗹";s:1:"l";s:4:"𝗺";s:1:"m";s:4:"𝗻";s:1:"n";s:4:"𝗼";s:1:"o";s:4:"𝗽";s:1:"p";s:4:"𝗾";s:1:"q";s:4:"𝗿";s:1:"r";s:4:"𝘀";s:1:"s";s:4:"𝘁";s:1:"t";s:4:"𝘂";s:1:"u";s:4:"𝘃";s:1:"v";s:4:"𝘄";s:1:"w";s:4:"𝘅";s:1:"x";s:4:"𝘆";s:1:"y";s:4:"𝘇";s:1:"z";s:4:"𝘈";s:1:"A";s:4:"𝘉";s:1:"B";s:4:"𝘊";s:1:"C";s:4:"𝘋";s:1:"D";s:4:"𝘌";s:1:"E";s:4:"𝘍";s:1:"F";s:4:"𝘎";s:1:"G";s:4:"𝘏";s:1:"H";s:4:"𝘐";s:1:"I";s:4:"𝘑";s:1:"J";s:4:"𝘒";s:1:"K";s:4:"𝘓";s:1:"L";s:4:"𝘔";s:1:"M";s:4:"𝘕";s:1:"N";s:4:"𝘖";s:1:"O";s:4:"𝘗";s:1:"P";s:4:"𝘘";s:1:"Q";s:4:"𝘙";s:1:"R";s:4:"𝘚";s:1:"S";s:4:"𝘛";s:1:"T";s:4:"𝘜";s:1:"U";s:4:"𝘝";s:1:"V";s:4:"𝘞";s:1:"W";s:4:"𝘟";s:1:"X";s:4:"𝘠";s:1:"Y";s:4:"𝘡";s:1:"Z";s:4:"𝘢";s:1:"a";s:4:"𝘣";s:1:"b";s:4:"𝘤";s:1:"c";s:4:"𝘥";s:1:"d";s:4:"𝘦";s:1:"e";s:4:"𝘧";s:1:"f";s:4:"𝘨";s:1:"g";s:4:"𝘩";s:1:"h";s:4:"𝘪";s:1:"i";s:4:"𝘫";s:1:"j";s:4:"𝘬";s:1:"k";s:4:"𝘭";s:1:"l";s:4:"𝘮";s:1:"m";s:4:"𝘯";s:1:"n";s:4:"𝘰";s:1:"o";s:4:"𝘱";s:1:"p";s:4:"𝘲";s:1:"q";s:4:"𝘳";s:1:"r";s:4:"𝘴";s:1:"s";s:4:"𝘵";s:1:"t";s:4:"𝘶";s:1:"u";s:4:"𝘷";s:1:"v";s:4:"𝘸";s:1:"w";s:4:"𝘹";s:1:"x";s:4:"𝘺";s:1:"y";s:4:"𝘻";s:1:"z";s:4:"𝘼";s:1:"A";s:4:"𝘽";s:1:"B";s:4:"𝘾";s:1:"C";s:4:"𝘿";s:1:"D";s:4:"𝙀";s:1:"E";s:4:"𝙁";s:1:"F";s:4:"𝙂";s:1:"G";s:4:"𝙃";s:1:"H";s:4:"𝙄";s:1:"I";s:4:"𝙅";s:1:"J";s:4:"𝙆";s:1:"K";s:4:"𝙇";s:1:"L";s:4:"𝙈";s:1:"M";s:4:"𝙉";s:1:"N";s:4:"𝙊";s:1:"O";s:4:"𝙋";s:1:"P";s:4:"𝙌";s:1:"Q";s:4:"𝙍";s:1:"R";s:4:"𝙎";s:1:"S";s:4:"𝙏";s:1:"T";s:4:"𝙐";s:1:"U";s:4:"𝙑";s:1:"V";s:4:"𝙒";s:1:"W";s:4:"𝙓";s:1:"X";s:4:"𝙔";s:1:"Y";s:4:"𝙕";s:1:"Z";s:4:"𝙖";s:1:"a";s:4:"𝙗";s:1:"b";s:4:"𝙘";s:1:"c";s:4:"𝙙";s:1:"d";s:4:"𝙚";s:1:"e";s:4:"𝙛";s:1:"f";s:4:"𝙜";s:1:"g";s:4:"𝙝";s:1:"h";s:4:"𝙞";s:1:"i";s:4:"𝙟";s:1:"j";s:4:"𝙠";s:1:"k";s:4:"𝙡";s:1:"l";s:4:"𝙢";s:1:"m";s:4:"𝙣";s:1:"n";s:4:"𝙤";s:1:"o";s:4:"𝙥";s:1:"p";s:4:"𝙦";s:1:"q";s:4:"𝙧";s:1:"r";s:4:"𝙨";s:1:"s";s:4:"𝙩";s:1:"t";s:4:"𝙪";s:1:"u";s:4:"𝙫";s:1:"v";s:4:"𝙬";s:1:"w";s:4:"𝙭";s:1:"x";s:4:"𝙮";s:1:"y";s:4:"𝙯";s:1:"z";s:4:"𝙰";s:1:"A";s:4:"𝙱";s:1:"B";s:4:"𝙲";s:1:"C";s:4:"𝙳";s:1:"D";s:4:"𝙴";s:1:"E";s:4:"𝙵";s:1:"F";s:4:"𝙶";s:1:"G";s:4:"𝙷";s:1:"H";s:4:"𝙸";s:1:"I";s:4:"𝙹";s:1:"J";s:4:"𝙺";s:1:"K";s:4:"𝙻";s:1:"L";s:4:"𝙼";s:1:"M";s:4:"𝙽";s:1:"N";s:4:"𝙾";s:1:"O";s:4:"𝙿";s:1:"P";s:4:"𝚀";s:1:"Q";s:4:"𝚁";s:1:"R";s:4:"𝚂";s:1:"S";s:4:"𝚃";s:1:"T";s:4:"𝚄";s:1:"U";s:4:"𝚅";s:1:"V";s:4:"𝚆";s:1:"W";s:4:"𝚇";s:1:"X";s:4:"𝚈";s:1:"Y";s:4:"𝚉";s:1:"Z";s:4:"𝚊";s:1:"a";s:4:"𝚋";s:1:"b";s:4:"𝚌";s:1:"c";s:4:"𝚍";s:1:"d";s:4:"𝚎";s:1:"e";s:4:"𝚏";s:1:"f";s:4:"𝚐";s:1:"g";s:4:"𝚑";s:1:"h";s:4:"𝚒";s:1:"i";s:4:"𝚓";s:1:"j";s:4:"𝚔";s:1:"k";s:4:"𝚕";s:1:"l";s:4:"𝚖";s:1:"m";s:4:"𝚗";s:1:"n";s:4:"𝚘";s:1:"o";s:4:"𝚙";s:1:"p";s:4:"𝚚";s:1:"q";s:4:"𝚛";s:1:"r";s:4:"𝚜";s:1:"s";s:4:"𝚝";s:1:"t";s:4:"𝚞";s:1:"u";s:4:"𝚟";s:1:"v";s:4:"𝚠";s:1:"w";s:4:"𝚡";s:1:"x";s:4:"𝚢";s:1:"y";s:4:"𝚣";s:1:"z";s:4:"𝚤";s:2:"ı";s:4:"𝚥";s:2:"ȷ";s:4:"𝚨";s:2:"Α";s:4:"𝚩";s:2:"Β";s:4:"𝚪";s:2:"Γ";s:4:"𝚫";s:2:"Δ";s:4:"𝚬";s:2:"Ε";s:4:"𝚭";s:2:"Ζ";s:4:"𝚮";s:2:"Η";s:4:"𝚯";s:2:"Θ";s:4:"𝚰";s:2:"Ι";s:4:"𝚱";s:2:"Κ";s:4:"𝚲";s:2:"Λ";s:4:"𝚳";s:2:"Μ";s:4:"𝚴";s:2:"Ν";s:4:"𝚵";s:2:"Ξ";s:4:"𝚶";s:2:"Ο";s:4:"𝚷";s:2:"Π";s:4:"𝚸";s:2:"Ρ";s:4:"𝚹";s:2:"Θ";s:4:"𝚺";s:2:"Σ";s:4:"𝚻";s:2:"Τ";s:4:"𝚼";s:2:"Υ";s:4:"𝚽";s:2:"Φ";s:4:"𝚾";s:2:"Χ";s:4:"𝚿";s:2:"Ψ";s:4:"𝛀";s:2:"Ω";s:4:"𝛁";s:3:"∇";s:4:"𝛂";s:2:"α";s:4:"𝛃";s:2:"β";s:4:"𝛄";s:2:"γ";s:4:"𝛅";s:2:"δ";s:4:"𝛆";s:2:"ε";s:4:"𝛇";s:2:"ζ";s:4:"𝛈";s:2:"η";s:4:"𝛉";s:2:"θ";s:4:"𝛊";s:2:"ι";s:4:"𝛋";s:2:"κ";s:4:"𝛌";s:2:"λ";s:4:"𝛍";s:2:"μ";s:4:"𝛎";s:2:"ν";s:4:"𝛏";s:2:"ξ";s:4:"𝛐";s:2:"ο";s:4:"𝛑";s:2:"π";s:4:"𝛒";s:2:"ρ";s:4:"𝛓";s:2:"ς";s:4:"𝛔";s:2:"σ";s:4:"𝛕";s:2:"τ";s:4:"𝛖";s:2:"υ";s:4:"𝛗";s:2:"φ";s:4:"𝛘";s:2:"χ";s:4:"𝛙";s:2:"ψ";s:4:"𝛚";s:2:"ω";s:4:"𝛛";s:3:"∂";s:4:"𝛜";s:2:"ε";s:4:"𝛝";s:2:"θ";s:4:"𝛞";s:2:"κ";s:4:"𝛟";s:2:"φ";s:4:"𝛠";s:2:"ρ";s:4:"𝛡";s:2:"π";s:4:"𝛢";s:2:"Α";s:4:"𝛣";s:2:"Β";s:4:"𝛤";s:2:"Γ";s:4:"𝛥";s:2:"Δ";s:4:"𝛦";s:2:"Ε";s:4:"𝛧";s:2:"Ζ";s:4:"𝛨";s:2:"Η";s:4:"𝛩";s:2:"Θ";s:4:"𝛪";s:2:"Ι";s:4:"𝛫";s:2:"Κ";s:4:"𝛬";s:2:"Λ";s:4:"𝛭";s:2:"Μ";s:4:"𝛮";s:2:"Ν";s:4:"𝛯";s:2:"Ξ";s:4:"𝛰";s:2:"Ο";s:4:"𝛱";s:2:"Π";s:4:"𝛲";s:2:"Ρ";s:4:"𝛳";s:2:"Θ";s:4:"𝛴";s:2:"Σ";s:4:"𝛵";s:2:"Τ";s:4:"𝛶";s:2:"Υ";s:4:"𝛷";s:2:"Φ";s:4:"𝛸";s:2:"Χ";s:4:"𝛹";s:2:"Ψ";s:4:"𝛺";s:2:"Ω";s:4:"𝛻";s:3:"∇";s:4:"𝛼";s:2:"α";s:4:"𝛽";s:2:"β";s:4:"𝛾";s:2:"γ";s:4:"𝛿";s:2:"δ";s:4:"𝜀";s:2:"ε";s:4:"𝜁";s:2:"ζ";s:4:"𝜂";s:2:"η";s:4:"𝜃";s:2:"θ";s:4:"𝜄";s:2:"ι";s:4:"𝜅";s:2:"κ";s:4:"𝜆";s:2:"λ";s:4:"𝜇";s:2:"μ";s:4:"𝜈";s:2:"ν";s:4:"𝜉";s:2:"ξ";s:4:"𝜊";s:2:"ο";s:4:"𝜋";s:2:"π";s:4:"𝜌";s:2:"ρ";s:4:"𝜍";s:2:"ς";s:4:"𝜎";s:2:"σ";s:4:"𝜏";s:2:"τ";s:4:"𝜐";s:2:"υ";s:4:"𝜑";s:2:"φ";s:4:"𝜒";s:2:"χ";s:4:"𝜓";s:2:"ψ";s:4:"𝜔";s:2:"ω";s:4:"𝜕";s:3:"∂";s:4:"𝜖";s:2:"ε";s:4:"𝜗";s:2:"θ";s:4:"𝜘";s:2:"κ";s:4:"𝜙";s:2:"φ";s:4:"𝜚";s:2:"ρ";s:4:"𝜛";s:2:"π";s:4:"𝜜";s:2:"Α";s:4:"𝜝";s:2:"Β";s:4:"𝜞";s:2:"Γ";s:4:"𝜟";s:2:"Δ";s:4:"𝜠";s:2:"Ε";s:4:"𝜡";s:2:"Ζ";s:4:"𝜢";s:2:"Η";s:4:"𝜣";s:2:"Θ";s:4:"𝜤";s:2:"Ι";s:4:"𝜥";s:2:"Κ";s:4:"𝜦";s:2:"Λ";s:4:"𝜧";s:2:"Μ";s:4:"𝜨";s:2:"Ν";s:4:"𝜩";s:2:"Ξ";s:4:"𝜪";s:2:"Ο";s:4:"𝜫";s:2:"Π";s:4:"𝜬";s:2:"Ρ";s:4:"𝜭";s:2:"Θ";s:4:"𝜮";s:2:"Σ";s:4:"𝜯";s:2:"Τ";s:4:"𝜰";s:2:"Υ";s:4:"𝜱";s:2:"Φ";s:4:"𝜲";s:2:"Χ";s:4:"𝜳";s:2:"Ψ";s:4:"𝜴";s:2:"Ω";s:4:"𝜵";s:3:"∇";s:4:"𝜶";s:2:"α";s:4:"𝜷";s:2:"β";s:4:"𝜸";s:2:"γ";s:4:"𝜹";s:2:"δ";s:4:"𝜺";s:2:"ε";s:4:"𝜻";s:2:"ζ";s:4:"𝜼";s:2:"η";s:4:"𝜽";s:2:"θ";s:4:"𝜾";s:2:"ι";s:4:"𝜿";s:2:"κ";s:4:"𝝀";s:2:"λ";s:4:"𝝁";s:2:"μ";s:4:"𝝂";s:2:"ν";s:4:"𝝃";s:2:"ξ";s:4:"𝝄";s:2:"ο";s:4:"𝝅";s:2:"π";s:4:"𝝆";s:2:"ρ";s:4:"𝝇";s:2:"ς";s:4:"𝝈";s:2:"σ";s:4:"𝝉";s:2:"τ";s:4:"𝝊";s:2:"υ";s:4:"𝝋";s:2:"φ";s:4:"𝝌";s:2:"χ";s:4:"𝝍";s:2:"ψ";s:4:"𝝎";s:2:"ω";s:4:"𝝏";s:3:"∂";s:4:"𝝐";s:2:"ε";s:4:"𝝑";s:2:"θ";s:4:"𝝒";s:2:"κ";s:4:"𝝓";s:2:"φ";s:4:"𝝔";s:2:"ρ";s:4:"𝝕";s:2:"π";s:4:"𝝖";s:2:"Α";s:4:"𝝗";s:2:"Β";s:4:"𝝘";s:2:"Γ";s:4:"𝝙";s:2:"Δ";s:4:"𝝚";s:2:"Ε";s:4:"𝝛";s:2:"Ζ";s:4:"𝝜";s:2:"Η";s:4:"𝝝";s:2:"Θ";s:4:"𝝞";s:2:"Ι";s:4:"𝝟";s:2:"Κ";s:4:"𝝠";s:2:"Λ";s:4:"𝝡";s:2:"Μ";s:4:"𝝢";s:2:"Ν";s:4:"𝝣";s:2:"Ξ";s:4:"𝝤";s:2:"Ο";s:4:"𝝥";s:2:"Π";s:4:"𝝦";s:2:"Ρ";s:4:"𝝧";s:2:"Θ";s:4:"𝝨";s:2:"Σ";s:4:"𝝩";s:2:"Τ";s:4:"𝝪";s:2:"Υ";s:4:"𝝫";s:2:"Φ";s:4:"𝝬";s:2:"Χ";s:4:"𝝭";s:2:"Ψ";s:4:"𝝮";s:2:"Ω";s:4:"𝝯";s:3:"∇";s:4:"𝝰";s:2:"α";s:4:"𝝱";s:2:"β";s:4:"𝝲";s:2:"γ";s:4:"𝝳";s:2:"δ";s:4:"𝝴";s:2:"ε";s:4:"𝝵";s:2:"ζ";s:4:"𝝶";s:2:"η";s:4:"𝝷";s:2:"θ";s:4:"𝝸";s:2:"ι";s:4:"𝝹";s:2:"κ";s:4:"𝝺";s:2:"λ";s:4:"𝝻";s:2:"μ";s:4:"𝝼";s:2:"ν";s:4:"𝝽";s:2:"ξ";s:4:"𝝾";s:2:"ο";s:4:"𝝿";s:2:"π";s:4:"𝞀";s:2:"ρ";s:4:"𝞁";s:2:"ς";s:4:"𝞂";s:2:"σ";s:4:"𝞃";s:2:"τ";s:4:"𝞄";s:2:"υ";s:4:"𝞅";s:2:"φ";s:4:"𝞆";s:2:"χ";s:4:"𝞇";s:2:"ψ";s:4:"𝞈";s:2:"ω";s:4:"𝞉";s:3:"∂";s:4:"𝞊";s:2:"ε";s:4:"𝞋";s:2:"θ";s:4:"𝞌";s:2:"κ";s:4:"𝞍";s:2:"φ";s:4:"𝞎";s:2:"ρ";s:4:"𝞏";s:2:"π";s:4:"𝞐";s:2:"Α";s:4:"𝞑";s:2:"Β";s:4:"𝞒";s:2:"Γ";s:4:"𝞓";s:2:"Δ";s:4:"𝞔";s:2:"Ε";s:4:"𝞕";s:2:"Ζ";s:4:"𝞖";s:2:"Η";s:4:"𝞗";s:2:"Θ";s:4:"𝞘";s:2:"Ι";s:4:"𝞙";s:2:"Κ";s:4:"𝞚";s:2:"Λ";s:4:"𝞛";s:2:"Μ";s:4:"𝞜";s:2:"Ν";s:4:"𝞝";s:2:"Ξ";s:4:"𝞞";s:2:"Ο";s:4:"𝞟";s:2:"Π";s:4:"𝞠";s:2:"Ρ";s:4:"𝞡";s:2:"Θ";s:4:"𝞢";s:2:"Σ";s:4:"𝞣";s:2:"Τ";s:4:"𝞤";s:2:"Υ";s:4:"𝞥";s:2:"Φ";s:4:"𝞦";s:2:"Χ";s:4:"𝞧";s:2:"Ψ";s:4:"𝞨";s:2:"Ω";s:4:"𝞩";s:3:"∇";s:4:"𝞪";s:2:"α";s:4:"𝞫";s:2:"β";s:4:"𝞬";s:2:"γ";s:4:"𝞭";s:2:"δ";s:4:"𝞮";s:2:"ε";s:4:"𝞯";s:2:"ζ";s:4:"𝞰";s:2:"η";s:4:"𝞱";s:2:"θ";s:4:"𝞲";s:2:"ι";s:4:"𝞳";s:2:"κ";s:4:"𝞴";s:2:"λ";s:4:"𝞵";s:2:"μ";s:4:"𝞶";s:2:"ν";s:4:"𝞷";s:2:"ξ";s:4:"𝞸";s:2:"ο";s:4:"𝞹";s:2:"π";s:4:"𝞺";s:2:"ρ";s:4:"𝞻";s:2:"ς";s:4:"𝞼";s:2:"σ";s:4:"𝞽";s:2:"τ";s:4:"𝞾";s:2:"υ";s:4:"𝞿";s:2:"φ";s:4:"𝟀";s:2:"χ";s:4:"𝟁";s:2:"ψ";s:4:"𝟂";s:2:"ω";s:4:"𝟃";s:3:"∂";s:4:"𝟄";s:2:"ε";s:4:"𝟅";s:2:"θ";s:4:"𝟆";s:2:"κ";s:4:"𝟇";s:2:"φ";s:4:"𝟈";s:2:"ρ";s:4:"𝟉";s:2:"π";s:4:"𝟊";s:2:"Ϝ";s:4:"𝟋";s:2:"ϝ";s:4:"𝟎";s:1:"0";s:4:"𝟏";s:1:"1";s:4:"𝟐";s:1:"2";s:4:"𝟑";s:1:"3";s:4:"𝟒";s:1:"4";s:4:"𝟓";s:1:"5";s:4:"𝟔";s:1:"6";s:4:"𝟕";s:1:"7";s:4:"𝟖";s:1:"8";s:4:"𝟗";s:1:"9";s:4:"𝟘";s:1:"0";s:4:"𝟙";s:1:"1";s:4:"𝟚";s:1:"2";s:4:"𝟛";s:1:"3";s:4:"𝟜";s:1:"4";s:4:"𝟝";s:1:"5";s:4:"𝟞";s:1:"6";s:4:"𝟟";s:1:"7";s:4:"𝟠";s:1:"8";s:4:"𝟡";s:1:"9";s:4:"𝟢";s:1:"0";s:4:"𝟣";s:1:"1";s:4:"𝟤";s:1:"2";s:4:"𝟥";s:1:"3";s:4:"𝟦";s:1:"4";s:4:"𝟧";s:1:"5";s:4:"𝟨";s:1:"6";s:4:"𝟩";s:1:"7";s:4:"𝟪";s:1:"8";s:4:"𝟫";s:1:"9";s:4:"𝟬";s:1:"0";s:4:"𝟭";s:1:"1";s:4:"𝟮";s:1:"2";s:4:"𝟯";s:1:"3";s:4:"𝟰";s:1:"4";s:4:"𝟱";s:1:"5";s:4:"𝟲";s:1:"6";s:4:"𝟳";s:1:"7";s:4:"𝟴";s:1:"8";s:4:"𝟵";s:1:"9";s:4:"𝟶";s:1:"0";s:4:"𝟷";s:1:"1";s:4:"𝟸";s:1:"2";s:4:"𝟹";s:1:"3";s:4:"𝟺";s:1:"4";s:4:"𝟻";s:1:"5";s:4:"𝟼";s:1:"6";s:4:"𝟽";s:1:"7";s:4:"𝟾";s:1:"8";s:4:"𝟿";s:1:"9";s:4:"丽";s:3:"丽";s:4:"丸";s:3:"丸";s:4:"乁";s:3:"乁";s:4:"𠄢";s:4:"𠄢";s:4:"你";s:3:"你";s:4:"侮";s:3:"侮";s:4:"侻";s:3:"侻";s:4:"倂";s:3:"倂";s:4:"偺";s:3:"偺";s:4:"備";s:3:"備";s:4:"僧";s:3:"僧";s:4:"像";s:3:"像";s:4:"㒞";s:3:"㒞";s:4:"𠘺";s:4:"𠘺";s:4:"免";s:3:"免";s:4:"兔";s:3:"兔";s:4:"兤";s:3:"兤";s:4:"具";s:3:"具";s:4:"𠔜";s:4:"𠔜";s:4:"㒹";s:3:"㒹";s:4:"內";s:3:"內";s:4:"再";s:3:"再";s:4:"𠕋";s:4:"𠕋";s:4:"冗";s:3:"冗";s:4:"冤";s:3:"冤";s:4:"仌";s:3:"仌";s:4:"冬";s:3:"冬";s:4:"况";s:3:"况";s:4:"𩇟";s:4:"𩇟";s:4:"凵";s:3:"凵";s:4:"刃";s:3:"刃";s:4:"㓟";s:3:"㓟";s:4:"刻";s:3:"刻";s:4:"剆";s:3:"剆";s:4:"割";s:3:"割";s:4:"剷";s:3:"剷";s:4:"㔕";s:3:"㔕";s:4:"勇";s:3:"勇";s:4:"勉";s:3:"勉";s:4:"勤";s:3:"勤";s:4:"勺";s:3:"勺";s:4:"包";s:3:"包";s:4:"匆";s:3:"匆";s:4:"北";s:3:"北";s:4:"卉";s:3:"卉";s:4:"卑";s:3:"卑";s:4:"博";s:3:"博";s:4:"即";s:3:"即";s:4:"卽";s:3:"卽";s:4:"卿";s:3:"卿";s:4:"卿";s:3:"卿";s:4:"卿";s:3:"卿";s:4:"𠨬";s:4:"𠨬";s:4:"灰";s:3:"灰";s:4:"及";s:3:"及";s:4:"叟";s:3:"叟";s:4:"𠭣";s:4:"𠭣";s:4:"叫";s:3:"叫";s:4:"叱";s:3:"叱";s:4:"吆";s:3:"吆";s:4:"咞";s:3:"咞";s:4:"吸";s:3:"吸";s:4:"呈";s:3:"呈";s:4:"周";s:3:"周";s:4:"咢";s:3:"咢";s:4:"哶";s:3:"哶";s:4:"唐";s:3:"唐";s:4:"啓";s:3:"啓";s:4:"啣";s:3:"啣";s:4:"善";s:3:"善";s:4:"善";s:3:"善";s:4:"喙";s:3:"喙";s:4:"喫";s:3:"喫";s:4:"喳";s:3:"喳";s:4:"嗂";s:3:"嗂";s:4:"圖";s:3:"圖";s:4:"嘆";s:3:"嘆";s:4:"圗";s:3:"圗";s:4:"噑";s:3:"噑";s:4:"噴";s:3:"噴";s:4:"切";s:3:"切";s:4:"壮";s:3:"壮";s:4:"城";s:3:"城";s:4:"埴";s:3:"埴";s:4:"堍";s:3:"堍";s:4:"型";s:3:"型";s:4:"堲";s:3:"堲";s:4:"報";s:3:"報";s:4:"墬";s:3:"墬";s:4:"𡓤";s:4:"𡓤";s:4:"売";s:3:"売";s:4:"壷";s:3:"壷";s:4:"夆";s:3:"夆";s:4:"多";s:3:"多";s:4:"夢";s:3:"夢";s:4:"奢";s:3:"奢";s:4:"𡚨";s:4:"𡚨";s:4:"𡛪";s:4:"𡛪";s:4:"姬";s:3:"姬";s:4:"娛";s:3:"娛";s:4:"娧";s:3:"娧";s:4:"姘";s:3:"姘";s:4:"婦";s:3:"婦";s:4:"㛮";s:3:"㛮";s:4:"㛼";s:3:"㛼";s:4:"嬈";s:3:"嬈";s:4:"嬾";s:3:"嬾";s:4:"嬾";s:3:"嬾";s:4:"𡧈";s:4:"𡧈";s:4:"寃";s:3:"寃";s:4:"寘";s:3:"寘";s:4:"寧";s:3:"寧";s:4:"寳";s:3:"寳";s:4:"𡬘";s:4:"𡬘";s:4:"寿";s:3:"寿";s:4:"将";s:3:"将";s:4:"当";s:3:"当";s:4:"尢";s:3:"尢";s:4:"㞁";s:3:"㞁";s:4:"屠";s:3:"屠";s:4:"屮";s:3:"屮";s:4:"峀";s:3:"峀";s:4:"岍";s:3:"岍";s:4:"𡷤";s:4:"𡷤";s:4:"嵃";s:3:"嵃";s:4:"𡷦";s:4:"𡷦";s:4:"嵮";s:3:"嵮";s:4:"嵫";s:3:"嵫";s:4:"嵼";s:3:"嵼";s:4:"巡";s:3:"巡";s:4:"巢";s:3:"巢";s:4:"㠯";s:3:"㠯";s:4:"巽";s:3:"巽";s:4:"帨";s:3:"帨";s:4:"帽";s:3:"帽";s:4:"幩";s:3:"幩";s:4:"㡢";s:3:"㡢";s:4:"𢆃";s:4:"𢆃";s:4:"㡼";s:3:"㡼";s:4:"庰";s:3:"庰";s:4:"庳";s:3:"庳";s:4:"庶";s:3:"庶";s:4:"廊";s:3:"廊";s:4:"𪎒";s:4:"𪎒";s:4:"廾";s:3:"廾";s:4:"𢌱";s:4:"𢌱";s:4:"𢌱";s:4:"𢌱";s:4:"舁";s:3:"舁";s:4:"弢";s:3:"弢";s:4:"弢";s:3:"弢";s:4:"㣇";s:3:"㣇";s:4:"𣊸";s:4:"𣊸";s:4:"𦇚";s:4:"𦇚";s:4:"形";s:3:"形";s:4:"彫";s:3:"彫";s:4:"㣣";s:3:"㣣";s:4:"徚";s:3:"徚";s:4:"忍";s:3:"忍";s:4:"志";s:3:"志";s:4:"忹";s:3:"忹";s:4:"悁";s:3:"悁";s:4:"㤺";s:3:"㤺";s:4:"㤜";s:3:"㤜";s:4:"悔";s:3:"悔";s:4:"𢛔";s:4:"𢛔";s:4:"惇";s:3:"惇";s:4:"慈";s:3:"慈";s:4:"慌";s:3:"慌";s:4:"慎";s:3:"慎";s:4:"慌";s:3:"慌";s:4:"慺";s:3:"慺";s:4:"憎";s:3:"憎";s:4:"憲";s:3:"憲";s:4:"憤";s:3:"憤";s:4:"憯";s:3:"憯";s:4:"懞";s:3:"懞";s:4:"懲";s:3:"懲";s:4:"懶";s:3:"懶";s:4:"成";s:3:"成";s:4:"戛";s:3:"戛";s:4:"扝";s:3:"扝";s:4:"抱";s:3:"抱";s:4:"拔";s:3:"拔";s:4:"捐";s:3:"捐";s:4:"𢬌";s:4:"𢬌";s:4:"挽";s:3:"挽";s:4:"拼";s:3:"拼";s:4:"捨";s:3:"捨";s:4:"掃";s:3:"掃";s:4:"揤";s:3:"揤";s:4:"𢯱";s:4:"𢯱";s:4:"搢";s:3:"搢";s:4:"揅";s:3:"揅";s:4:"掩";s:3:"掩";s:4:"㨮";s:3:"㨮";s:4:"摩";s:3:"摩";s:4:"摾";s:3:"摾";s:4:"撝";s:3:"撝";s:4:"摷";s:3:"摷";s:4:"㩬";s:3:"㩬";s:4:"敏";s:3:"敏";s:4:"敬";s:3:"敬";s:4:"𣀊";s:4:"𣀊";s:4:"旣";s:3:"旣";s:4:"書";s:3:"書";s:4:"晉";s:3:"晉";s:4:"㬙";s:3:"㬙";s:4:"暑";s:3:"暑";s:4:"㬈";s:3:"㬈";s:4:"㫤";s:3:"㫤";s:4:"冒";s:3:"冒";s:4:"冕";s:3:"冕";s:4:"最";s:3:"最";s:4:"暜";s:3:"暜";s:4:"肭";s:3:"肭";s:4:"䏙";s:3:"䏙";s:4:"朗";s:3:"朗";s:4:"望";s:3:"望";s:4:"朡";s:3:"朡";s:4:"杞";s:3:"杞";s:4:"杓";s:3:"杓";s:4:"𣏃";s:4:"𣏃";s:4:"㭉";s:3:"㭉";s:4:"柺";s:3:"柺";s:4:"枅";s:3:"枅";s:4:"桒";s:3:"桒";s:4:"梅";s:3:"梅";s:4:"𣑭";s:4:"𣑭";s:4:"梎";s:3:"梎";s:4:"栟";s:3:"栟";s:4:"椔";s:3:"椔";s:4:"㮝";s:3:"㮝";s:4:"楂";s:3:"楂";s:4:"榣";s:3:"榣";s:4:"槪";s:3:"槪";s:4:"檨";s:3:"檨";s:4:"𣚣";s:4:"𣚣";s:4:"櫛";s:3:"櫛";s:4:"㰘";s:3:"㰘";s:4:"次";s:3:"次";s:4:"𣢧";s:4:"𣢧";s:4:"歔";s:3:"歔";s:4:"㱎";s:3:"㱎";s:4:"歲";s:3:"歲";s:4:"殟";s:3:"殟";s:4:"殺";s:3:"殺";s:4:"殻";s:3:"殻";s:4:"𣪍";s:4:"𣪍";s:4:"𡴋";s:4:"𡴋";s:4:"𣫺";s:4:"𣫺";s:4:"汎";s:3:"汎";s:4:"𣲼";s:4:"𣲼";s:4:"沿";s:3:"沿";s:4:"泍";s:3:"泍";s:4:"汧";s:3:"汧";s:4:"洖";s:3:"洖";s:4:"派";s:3:"派";s:4:"海";s:3:"海";s:4:"流";s:3:"流";s:4:"浩";s:3:"浩";s:4:"浸";s:3:"浸";s:4:"涅";s:3:"涅";s:4:"𣴞";s:4:"𣴞";s:4:"洴";s:3:"洴";s:4:"港";s:3:"港";s:4:"湮";s:3:"湮";s:4:"㴳";s:3:"㴳";s:4:"滋";s:3:"滋";s:4:"滇";s:3:"滇";s:4:"𣻑";s:4:"𣻑";s:4:"淹";s:3:"淹";s:4:"潮";s:3:"潮";s:4:"𣽞";s:4:"𣽞";s:4:"𣾎";s:4:"𣾎";s:4:"濆";s:3:"濆";s:4:"瀹";s:3:"瀹";s:4:"瀞";s:3:"瀞";s:4:"瀛";s:3:"瀛";s:4:"㶖";s:3:"㶖";s:4:"灊";s:3:"灊";s:4:"災";s:3:"災";s:4:"灷";s:3:"灷";s:4:"炭";s:3:"炭";s:4:"𠔥";s:4:"𠔥";s:4:"煅";s:3:"煅";s:4:"𤉣";s:4:"𤉣";s:4:"熜";s:3:"熜";s:4:"𤎫";s:4:"𤎫";s:4:"爨";s:3:"爨";s:4:"爵";s:3:"爵";s:4:"牐";s:3:"牐";s:4:"𤘈";s:4:"𤘈";s:4:"犀";s:3:"犀";s:4:"犕";s:3:"犕";s:4:"𤜵";s:4:"𤜵";s:4:"𤠔";s:4:"𤠔";s:4:"獺";s:3:"獺";s:4:"王";s:3:"王";s:4:"㺬";s:3:"㺬";s:4:"玥";s:3:"玥";s:4:"㺸";s:3:"㺸";s:4:"㺸";s:3:"㺸";s:4:"瑇";s:3:"瑇";s:4:"瑜";s:3:"瑜";s:4:"瑱";s:3:"瑱";s:4:"璅";s:3:"璅";s:4:"瓊";s:3:"瓊";s:4:"㼛";s:3:"㼛";s:4:"甤";s:3:"甤";s:4:"𤰶";s:4:"𤰶";s:4:"甾";s:3:"甾";s:4:"𤲒";s:4:"𤲒";s:4:"異";s:3:"異";s:4:"𢆟";s:4:"𢆟";s:4:"瘐";s:3:"瘐";s:4:"𤾡";s:4:"𤾡";s:4:"𤾸";s:4:"𤾸";s:4:"𥁄";s:4:"𥁄";s:4:"㿼";s:3:"㿼";s:4:"䀈";s:3:"䀈";s:4:"直";s:3:"直";s:4:"𥃳";s:4:"𥃳";s:4:"𥃲";s:4:"𥃲";s:4:"𥄙";s:4:"𥄙";s:4:"𥄳";s:4:"𥄳";s:4:"眞";s:3:"眞";s:4:"真";s:3:"真";s:4:"真";s:3:"真";s:4:"睊";s:3:"睊";s:4:"䀹";s:3:"䀹";s:4:"瞋";s:3:"瞋";s:4:"䁆";s:3:"䁆";s:4:"䂖";s:3:"䂖";s:4:"𥐝";s:4:"𥐝";s:4:"硎";s:3:"硎";s:4:"碌";s:3:"碌";s:4:"磌";s:3:"磌";s:4:"䃣";s:3:"䃣";s:4:"𥘦";s:4:"𥘦";s:4:"祖";s:3:"祖";s:4:"𥚚";s:4:"𥚚";s:4:"𥛅";s:4:"𥛅";s:4:"福";s:3:"福";s:4:"秫";s:3:"秫";s:4:"䄯";s:3:"䄯";s:4:"穀";s:3:"穀";s:4:"穊";s:3:"穊";s:4:"穏";s:3:"穏";s:4:"𥥼";s:4:"𥥼";s:4:"𥪧";s:4:"𥪧";s:4:"𥪧";s:4:"𥪧";s:4:"竮";s:3:"竮";s:4:"䈂";s:3:"䈂";s:4:"𥮫";s:4:"𥮫";s:4:"篆";s:3:"篆";s:4:"築";s:3:"築";s:4:"䈧";s:3:"䈧";s:4:"𥲀";s:4:"𥲀";s:4:"糒";s:3:"糒";s:4:"䊠";s:3:"䊠";s:4:"糨";s:3:"糨";s:4:"糣";s:3:"糣";s:4:"紀";s:3:"紀";s:4:"𥾆";s:4:"𥾆";s:4:"絣";s:3:"絣";s:4:"䌁";s:3:"䌁";s:4:"緇";s:3:"緇";s:4:"縂";s:3:"縂";s:4:"繅";s:3:"繅";s:4:"䌴";s:3:"䌴";s:4:"𦈨";s:4:"𦈨";s:4:"𦉇";s:4:"𦉇";s:4:"䍙";s:3:"䍙";s:4:"𦋙";s:4:"𦋙";s:4:"罺";s:3:"罺";s:4:"𦌾";s:4:"𦌾";s:4:"羕";s:3:"羕";s:4:"翺";s:3:"翺";s:4:"者";s:3:"者";s:4:"𦓚";s:4:"𦓚";s:4:"𦔣";s:4:"𦔣";s:4:"聠";s:3:"聠";s:4:"𦖨";s:4:"𦖨";s:4:"聰";s:3:"聰";s:4:"𣍟";s:4:"𣍟";s:4:"䏕";s:3:"䏕";s:4:"育";s:3:"育";s:4:"脃";s:3:"脃";s:4:"䐋";s:3:"䐋";s:4:"脾";s:3:"脾";s:4:"媵";s:3:"媵";s:4:"𦞧";s:4:"𦞧";s:4:"𦞵";s:4:"𦞵";s:4:"𣎓";s:4:"𣎓";s:4:"𣎜";s:4:"𣎜";s:4:"舁";s:3:"舁";s:4:"舄";s:3:"舄";s:4:"辞";s:3:"辞";s:4:"䑫";s:3:"䑫";s:4:"芑";s:3:"芑";s:4:"芋";s:3:"芋";s:4:"芝";s:3:"芝";s:4:"劳";s:3:"劳";s:4:"花";s:3:"花";s:4:"芳";s:3:"芳";s:4:"芽";s:3:"芽";s:4:"苦";s:3:"苦";s:4:"𦬼";s:4:"𦬼";s:4:"若";s:3:"若";s:4:"茝";s:3:"茝";s:4:"荣";s:3:"荣";s:4:"莭";s:3:"莭";s:4:"茣";s:3:"茣";s:4:"莽";s:3:"莽";s:4:"菧";s:3:"菧";s:4:"著";s:3:"著";s:4:"荓";s:3:"荓";s:4:"菊";s:3:"菊";s:4:"菌";s:3:"菌";s:4:"菜";s:3:"菜";s:4:"𦰶";s:4:"𦰶";s:4:"𦵫";s:4:"𦵫";s:4:"𦳕";s:4:"𦳕";s:4:"䔫";s:3:"䔫";s:4:"蓱";s:3:"蓱";s:4:"蓳";s:3:"蓳";s:4:"蔖";s:3:"蔖";s:4:"𧏊";s:4:"𧏊";s:4:"蕤";s:3:"蕤";s:4:"𦼬";s:4:"𦼬";s:4:"䕝";s:3:"䕝";s:4:"䕡";s:3:"䕡";s:4:"𦾱";s:4:"𦾱";s:4:"𧃒";s:4:"𧃒";s:4:"䕫";s:3:"䕫";s:4:"虐";s:3:"虐";s:4:"虜";s:3:"虜";s:4:"虧";s:3:"虧";s:4:"虩";s:3:"虩";s:4:"蚩";s:3:"蚩";s:4:"蚈";s:3:"蚈";s:4:"蜎";s:3:"蜎";s:4:"蛢";s:3:"蛢";s:4:"蝹";s:3:"蝹";s:4:"蜨";s:3:"蜨";s:4:"蝫";s:3:"蝫";s:4:"螆";s:3:"螆";s:4:"䗗";s:3:"䗗";s:4:"蟡";s:3:"蟡";s:4:"蠁";s:3:"蠁";s:4:"䗹";s:3:"䗹";s:4:"衠";s:3:"衠";s:4:"衣";s:3:"衣";s:4:"𧙧";s:4:"𧙧";s:4:"裗";s:3:"裗";s:4:"裞";s:3:"裞";s:4:"䘵";s:3:"䘵";s:4:"裺";s:3:"裺";s:4:"㒻";s:3:"㒻";s:4:"𧢮";s:4:"𧢮";s:4:"𧥦";s:4:"𧥦";s:4:"䚾";s:3:"䚾";s:4:"䛇";s:3:"䛇";s:4:"誠";s:3:"誠";s:4:"諭";s:3:"諭";s:4:"變";s:3:"變";s:4:"豕";s:3:"豕";s:4:"𧲨";s:4:"𧲨";s:4:"貫";s:3:"貫";s:4:"賁";s:3:"賁";s:4:"贛";s:3:"贛";s:4:"起";s:3:"起";s:4:"𧼯";s:4:"𧼯";s:4:"𠠄";s:4:"𠠄";s:4:"跋";s:3:"跋";s:4:"趼";s:3:"趼";s:4:"跰";s:3:"跰";s:4:"𠣞";s:4:"𠣞";s:4:"軔";s:3:"軔";s:4:"輸";s:3:"輸";s:4:"𨗒";s:4:"𨗒";s:4:"𨗭";s:4:"𨗭";s:4:"邔";s:3:"邔";s:4:"郱";s:3:"郱";s:4:"鄑";s:3:"鄑";s:4:"𨜮";s:4:"𨜮";s:4:"鄛";s:3:"鄛";s:4:"鈸";s:3:"鈸";s:4:"鋗";s:3:"鋗";s:4:"鋘";s:3:"鋘";s:4:"鉼";s:3:"鉼";s:4:"鏹";s:3:"鏹";s:4:"鐕";s:3:"鐕";s:4:"𨯺";s:4:"𨯺";s:4:"開";s:3:"開";s:4:"䦕";s:3:"䦕";s:4:"閷";s:3:"閷";s:4:"𨵷";s:4:"𨵷";s:4:"䧦";s:3:"䧦";s:4:"雃";s:3:"雃";s:4:"嶲";s:3:"嶲";s:4:"霣";s:3:"霣";s:4:"𩅅";s:4:"𩅅";s:4:"𩈚";s:4:"𩈚";s:4:"䩮";s:3:"䩮";s:4:"䩶";s:3:"䩶";s:4:"韠";s:3:"韠";s:4:"𩐊";s:4:"𩐊";s:4:"䪲";s:3:"䪲";s:4:"𩒖";s:4:"𩒖";s:4:"頋";s:3:"頋";s:4:"頋";s:3:"頋";s:4:"頩";s:3:"頩";s:4:"𩖶";s:4:"𩖶";s:4:"飢";s:3:"飢";s:4:"䬳";s:3:"䬳";s:4:"餩";s:3:"餩";s:4:"馧";s:3:"馧";s:4:"駂";s:3:"駂";s:4:"駾";s:3:"駾";s:4:"䯎";s:3:"䯎";s:4:"𩬰";s:4:"𩬰";s:4:"鬒";s:3:"鬒";s:4:"鱀";s:3:"鱀";s:4:"鳽";s:3:"鳽";s:4:"䳎";s:3:"䳎";s:4:"䳭";s:3:"䳭";s:4:"鵧";s:3:"鵧";s:4:"𪃎";s:4:"𪃎";s:4:"䳸";s:3:"䳸";s:4:"𪄅";s:4:"𪄅";s:4:"𪈎";s:4:"𪈎";s:4:"𪊑";s:4:"𪊑";s:4:"麻";s:3:"麻";s:4:"䵖";s:3:"䵖";s:4:"黹";s:3:"黹";s:4:"黾";s:3:"黾";s:4:"鼅";s:3:"鼅";s:4:"鼏";s:3:"鼏";s:4:"鼖";s:3:"鼖";s:4:"鼻";s:3:"鼻";s:4:"𪘀";s:4:"𪘀";}' );
+$utfCompatibilityDecomp = unserialize( 'a:5405:{s:2:" ";s:1:" ";s:2:"¨";s:3:" ̈";s:2:"ª";s:1:"a";s:2:"¯";s:3:" ̄";s:2:"²";s:1:"2";s:2:"³";s:1:"3";s:2:"´";s:3:" ́";s:2:"µ";s:2:"μ";s:2:"¸";s:3:" ̧";s:2:"¹";s:1:"1";s:2:"º";s:1:"o";s:2:"¼";s:5:"1⁄4";s:2:"½";s:5:"1⁄2";s:2:"¾";s:5:"3⁄4";s:2:"À";s:3:"À";s:2:"Á";s:3:"Á";s:2:"Â";s:3:"Â";s:2:"Ã";s:3:"Ã";s:2:"Ä";s:3:"Ä";s:2:"Å";s:3:"Å";s:2:"Ç";s:3:"Ç";s:2:"È";s:3:"È";s:2:"É";s:3:"É";s:2:"Ê";s:3:"Ê";s:2:"Ë";s:3:"Ë";s:2:"Ì";s:3:"Ì";s:2:"Í";s:3:"Í";s:2:"Î";s:3:"Î";s:2:"Ï";s:3:"Ï";s:2:"Ñ";s:3:"Ñ";s:2:"Ò";s:3:"Ò";s:2:"Ó";s:3:"Ó";s:2:"Ô";s:3:"Ô";s:2:"Õ";s:3:"Õ";s:2:"Ö";s:3:"Ö";s:2:"Ù";s:3:"Ù";s:2:"Ú";s:3:"Ú";s:2:"Û";s:3:"Û";s:2:"Ü";s:3:"Ü";s:2:"Ý";s:3:"Ý";s:2:"à";s:3:"à";s:2:"á";s:3:"á";s:2:"â";s:3:"â";s:2:"ã";s:3:"ã";s:2:"ä";s:3:"ä";s:2:"å";s:3:"å";s:2:"ç";s:3:"ç";s:2:"è";s:3:"è";s:2:"é";s:3:"é";s:2:"ê";s:3:"ê";s:2:"ë";s:3:"ë";s:2:"ì";s:3:"ì";s:2:"í";s:3:"í";s:2:"î";s:3:"î";s:2:"ï";s:3:"ï";s:2:"ñ";s:3:"ñ";s:2:"ò";s:3:"ò";s:2:"ó";s:3:"ó";s:2:"ô";s:3:"ô";s:2:"õ";s:3:"õ";s:2:"ö";s:3:"ö";s:2:"ù";s:3:"ù";s:2:"ú";s:3:"ú";s:2:"û";s:3:"û";s:2:"ü";s:3:"ü";s:2:"ý";s:3:"ý";s:2:"ÿ";s:3:"ÿ";s:2:"Ā";s:3:"Ā";s:2:"ā";s:3:"ā";s:2:"Ă";s:3:"Ă";s:2:"ă";s:3:"ă";s:2:"Ą";s:3:"Ą";s:2:"ą";s:3:"ą";s:2:"Ć";s:3:"Ć";s:2:"ć";s:3:"ć";s:2:"Ĉ";s:3:"Ĉ";s:2:"ĉ";s:3:"ĉ";s:2:"Ċ";s:3:"Ċ";s:2:"ċ";s:3:"ċ";s:2:"Č";s:3:"Č";s:2:"č";s:3:"č";s:2:"Ď";s:3:"Ď";s:2:"ď";s:3:"ď";s:2:"Ē";s:3:"Ē";s:2:"ē";s:3:"ē";s:2:"Ĕ";s:3:"Ĕ";s:2:"ĕ";s:3:"ĕ";s:2:"Ė";s:3:"Ė";s:2:"ė";s:3:"ė";s:2:"Ę";s:3:"Ę";s:2:"ę";s:3:"ę";s:2:"Ě";s:3:"Ě";s:2:"ě";s:3:"ě";s:2:"Ĝ";s:3:"Ĝ";s:2:"ĝ";s:3:"ĝ";s:2:"Ğ";s:3:"Ğ";s:2:"ğ";s:3:"ğ";s:2:"Ġ";s:3:"Ġ";s:2:"ġ";s:3:"ġ";s:2:"Ģ";s:3:"Ģ";s:2:"ģ";s:3:"ģ";s:2:"Ĥ";s:3:"Ĥ";s:2:"ĥ";s:3:"ĥ";s:2:"Ĩ";s:3:"Ĩ";s:2:"ĩ";s:3:"ĩ";s:2:"Ī";s:3:"Ī";s:2:"ī";s:3:"ī";s:2:"Ĭ";s:3:"Ĭ";s:2:"ĭ";s:3:"ĭ";s:2:"Į";s:3:"Į";s:2:"į";s:3:"į";s:2:"İ";s:3:"İ";s:2:"IJ";s:2:"IJ";s:2:"ij";s:2:"ij";s:2:"Ĵ";s:3:"Ĵ";s:2:"ĵ";s:3:"ĵ";s:2:"Ķ";s:3:"Ķ";s:2:"ķ";s:3:"ķ";s:2:"Ĺ";s:3:"Ĺ";s:2:"ĺ";s:3:"ĺ";s:2:"Ļ";s:3:"Ļ";s:2:"ļ";s:3:"ļ";s:2:"Ľ";s:3:"Ľ";s:2:"ľ";s:3:"ľ";s:2:"Ŀ";s:3:"L·";s:2:"ŀ";s:3:"l·";s:2:"Ń";s:3:"Ń";s:2:"ń";s:3:"ń";s:2:"Ņ";s:3:"Ņ";s:2:"ņ";s:3:"ņ";s:2:"Ň";s:3:"Ň";s:2:"ň";s:3:"ň";s:2:"ʼn";s:3:"ʼn";s:2:"Ō";s:3:"Ō";s:2:"ō";s:3:"ō";s:2:"Ŏ";s:3:"Ŏ";s:2:"ŏ";s:3:"ŏ";s:2:"Ő";s:3:"Ő";s:2:"ő";s:3:"ő";s:2:"Ŕ";s:3:"Ŕ";s:2:"ŕ";s:3:"ŕ";s:2:"Ŗ";s:3:"Ŗ";s:2:"ŗ";s:3:"ŗ";s:2:"Ř";s:3:"Ř";s:2:"ř";s:3:"ř";s:2:"Ś";s:3:"Ś";s:2:"ś";s:3:"ś";s:2:"Ŝ";s:3:"Ŝ";s:2:"ŝ";s:3:"ŝ";s:2:"Ş";s:3:"Ş";s:2:"ş";s:3:"ş";s:2:"Š";s:3:"Š";s:2:"š";s:3:"š";s:2:"Ţ";s:3:"Ţ";s:2:"ţ";s:3:"ţ";s:2:"Ť";s:3:"Ť";s:2:"ť";s:3:"ť";s:2:"Ũ";s:3:"Ũ";s:2:"ũ";s:3:"ũ";s:2:"Ū";s:3:"Ū";s:2:"ū";s:3:"ū";s:2:"Ŭ";s:3:"Ŭ";s:2:"ŭ";s:3:"ŭ";s:2:"Ů";s:3:"Ů";s:2:"ů";s:3:"ů";s:2:"Ű";s:3:"Ű";s:2:"ű";s:3:"ű";s:2:"Ų";s:3:"Ų";s:2:"ų";s:3:"ų";s:2:"Ŵ";s:3:"Ŵ";s:2:"ŵ";s:3:"ŵ";s:2:"Ŷ";s:3:"Ŷ";s:2:"ŷ";s:3:"ŷ";s:2:"Ÿ";s:3:"Ÿ";s:2:"Ź";s:3:"Ź";s:2:"ź";s:3:"ź";s:2:"Ż";s:3:"Ż";s:2:"ż";s:3:"ż";s:2:"Ž";s:3:"Ž";s:2:"ž";s:3:"ž";s:2:"ſ";s:1:"s";s:2:"Ơ";s:3:"Ơ";s:2:"ơ";s:3:"ơ";s:2:"Ư";s:3:"Ư";s:2:"ư";s:3:"ư";s:2:"DŽ";s:4:"DŽ";s:2:"Dž";s:4:"Dž";s:2:"dž";s:4:"dž";s:2:"LJ";s:2:"LJ";s:2:"Lj";s:2:"Lj";s:2:"lj";s:2:"lj";s:2:"NJ";s:2:"NJ";s:2:"Nj";s:2:"Nj";s:2:"nj";s:2:"nj";s:2:"Ǎ";s:3:"Ǎ";s:2:"ǎ";s:3:"ǎ";s:2:"Ǐ";s:3:"Ǐ";s:2:"ǐ";s:3:"ǐ";s:2:"Ǒ";s:3:"Ǒ";s:2:"ǒ";s:3:"ǒ";s:2:"Ǔ";s:3:"Ǔ";s:2:"ǔ";s:3:"ǔ";s:2:"Ǖ";s:5:"Ǖ";s:2:"ǖ";s:5:"ǖ";s:2:"Ǘ";s:5:"Ǘ";s:2:"ǘ";s:5:"ǘ";s:2:"Ǚ";s:5:"Ǚ";s:2:"ǚ";s:5:"ǚ";s:2:"Ǜ";s:5:"Ǜ";s:2:"ǜ";s:5:"ǜ";s:2:"Ǟ";s:5:"Ǟ";s:2:"ǟ";s:5:"ǟ";s:2:"Ǡ";s:5:"Ǡ";s:2:"ǡ";s:5:"ǡ";s:2:"Ǣ";s:4:"Ǣ";s:2:"ǣ";s:4:"ǣ";s:2:"Ǧ";s:3:"Ǧ";s:2:"ǧ";s:3:"ǧ";s:2:"Ǩ";s:3:"Ǩ";s:2:"ǩ";s:3:"ǩ";s:2:"Ǫ";s:3:"Ǫ";s:2:"ǫ";s:3:"ǫ";s:2:"Ǭ";s:5:"Ǭ";s:2:"ǭ";s:5:"ǭ";s:2:"Ǯ";s:4:"Ǯ";s:2:"ǯ";s:4:"ǯ";s:2:"ǰ";s:3:"ǰ";s:2:"DZ";s:2:"DZ";s:2:"Dz";s:2:"Dz";s:2:"dz";s:2:"dz";s:2:"Ǵ";s:3:"Ǵ";s:2:"ǵ";s:3:"ǵ";s:2:"Ǹ";s:3:"Ǹ";s:2:"ǹ";s:3:"ǹ";s:2:"Ǻ";s:5:"Ǻ";s:2:"ǻ";s:5:"ǻ";s:2:"Ǽ";s:4:"Ǽ";s:2:"ǽ";s:4:"ǽ";s:2:"Ǿ";s:4:"Ǿ";s:2:"ǿ";s:4:"ǿ";s:2:"Ȁ";s:3:"Ȁ";s:2:"ȁ";s:3:"ȁ";s:2:"Ȃ";s:3:"Ȃ";s:2:"ȃ";s:3:"ȃ";s:2:"Ȅ";s:3:"Ȅ";s:2:"ȅ";s:3:"ȅ";s:2:"Ȇ";s:3:"Ȇ";s:2:"ȇ";s:3:"ȇ";s:2:"Ȉ";s:3:"Ȉ";s:2:"ȉ";s:3:"ȉ";s:2:"Ȋ";s:3:"Ȋ";s:2:"ȋ";s:3:"ȋ";s:2:"Ȍ";s:3:"Ȍ";s:2:"ȍ";s:3:"ȍ";s:2:"Ȏ";s:3:"Ȏ";s:2:"ȏ";s:3:"ȏ";s:2:"Ȑ";s:3:"Ȑ";s:2:"ȑ";s:3:"ȑ";s:2:"Ȓ";s:3:"Ȓ";s:2:"ȓ";s:3:"ȓ";s:2:"Ȕ";s:3:"Ȕ";s:2:"ȕ";s:3:"ȕ";s:2:"Ȗ";s:3:"Ȗ";s:2:"ȗ";s:3:"ȗ";s:2:"Ș";s:3:"Ș";s:2:"ș";s:3:"ș";s:2:"Ț";s:3:"Ț";s:2:"ț";s:3:"ț";s:2:"Ȟ";s:3:"Ȟ";s:2:"ȟ";s:3:"ȟ";s:2:"Ȧ";s:3:"Ȧ";s:2:"ȧ";s:3:"ȧ";s:2:"Ȩ";s:3:"Ȩ";s:2:"ȩ";s:3:"ȩ";s:2:"Ȫ";s:5:"Ȫ";s:2:"ȫ";s:5:"ȫ";s:2:"Ȭ";s:5:"Ȭ";s:2:"ȭ";s:5:"ȭ";s:2:"Ȯ";s:3:"Ȯ";s:2:"ȯ";s:3:"ȯ";s:2:"Ȱ";s:5:"Ȱ";s:2:"ȱ";s:5:"ȱ";s:2:"Ȳ";s:3:"Ȳ";s:2:"ȳ";s:3:"ȳ";s:2:"ʰ";s:1:"h";s:2:"ʱ";s:2:"ɦ";s:2:"ʲ";s:1:"j";s:2:"ʳ";s:1:"r";s:2:"ʴ";s:2:"ɹ";s:2:"ʵ";s:2:"ɻ";s:2:"ʶ";s:2:"ʁ";s:2:"ʷ";s:1:"w";s:2:"ʸ";s:1:"y";s:2:"˘";s:3:" ̆";s:2:"˙";s:3:" ̇";s:2:"˚";s:3:" ̊";s:2:"˛";s:3:" ̨";s:2:"˜";s:3:" ̃";s:2:"˝";s:3:" ̋";s:2:"ˠ";s:2:"ɣ";s:2:"ˡ";s:1:"l";s:2:"ˢ";s:1:"s";s:2:"ˣ";s:1:"x";s:2:"ˤ";s:2:"ʕ";s:2:"̀";s:2:"̀";s:2:"́";s:2:"́";s:2:"̓";s:2:"̓";s:2:"̈́";s:4:"̈́";s:2:"ʹ";s:2:"ʹ";s:2:"ͺ";s:3:" ͅ";s:2:";";s:1:";";s:2:"΄";s:3:" ́";s:2:"΅";s:5:" ̈́";s:2:"Ά";s:4:"Ά";s:2:"·";s:2:"·";s:2:"Έ";s:4:"Έ";s:2:"Ή";s:4:"Ή";s:2:"Ί";s:4:"Ί";s:2:"Ό";s:4:"Ό";s:2:"Ύ";s:4:"Ύ";s:2:"Ώ";s:4:"Ώ";s:2:"ΐ";s:6:"ΐ";s:2:"Ϊ";s:4:"Ϊ";s:2:"Ϋ";s:4:"Ϋ";s:2:"ά";s:4:"ά";s:2:"έ";s:4:"έ";s:2:"ή";s:4:"ή";s:2:"ί";s:4:"ί";s:2:"ΰ";s:6:"ΰ";s:2:"ϊ";s:4:"ϊ";s:2:"ϋ";s:4:"ϋ";s:2:"ό";s:4:"ό";s:2:"ύ";s:4:"ύ";s:2:"ώ";s:4:"ώ";s:2:"ϐ";s:2:"β";s:2:"ϑ";s:2:"θ";s:2:"ϒ";s:2:"Υ";s:2:"ϓ";s:4:"Ύ";s:2:"ϔ";s:4:"Ϋ";s:2:"ϕ";s:2:"φ";s:2:"ϖ";s:2:"π";s:2:"ϰ";s:2:"κ";s:2:"ϱ";s:2:"ρ";s:2:"ϲ";s:2:"ς";s:2:"ϴ";s:2:"Θ";s:2:"ϵ";s:2:"ε";s:2:"Ϲ";s:2:"Σ";s:2:"Ѐ";s:4:"Ѐ";s:2:"Ё";s:4:"Ё";s:2:"Ѓ";s:4:"Ѓ";s:2:"Ї";s:4:"Ї";s:2:"Ќ";s:4:"Ќ";s:2:"Ѝ";s:4:"Ѝ";s:2:"Ў";s:4:"Ў";s:2:"Й";s:4:"Й";s:2:"й";s:4:"й";s:2:"ѐ";s:4:"ѐ";s:2:"ё";s:4:"ё";s:2:"ѓ";s:4:"ѓ";s:2:"ї";s:4:"ї";s:2:"ќ";s:4:"ќ";s:2:"ѝ";s:4:"ѝ";s:2:"ў";s:4:"ў";s:2:"Ѷ";s:4:"Ѷ";s:2:"ѷ";s:4:"ѷ";s:2:"Ӂ";s:4:"Ӂ";s:2:"ӂ";s:4:"ӂ";s:2:"Ӑ";s:4:"Ӑ";s:2:"ӑ";s:4:"ӑ";s:2:"Ӓ";s:4:"Ӓ";s:2:"ӓ";s:4:"ӓ";s:2:"Ӗ";s:4:"Ӗ";s:2:"ӗ";s:4:"ӗ";s:2:"Ӛ";s:4:"Ӛ";s:2:"ӛ";s:4:"ӛ";s:2:"Ӝ";s:4:"Ӝ";s:2:"ӝ";s:4:"ӝ";s:2:"Ӟ";s:4:"Ӟ";s:2:"ӟ";s:4:"ӟ";s:2:"Ӣ";s:4:"Ӣ";s:2:"ӣ";s:4:"ӣ";s:2:"Ӥ";s:4:"Ӥ";s:2:"ӥ";s:4:"ӥ";s:2:"Ӧ";s:4:"Ӧ";s:2:"ӧ";s:4:"ӧ";s:2:"Ӫ";s:4:"Ӫ";s:2:"ӫ";s:4:"ӫ";s:2:"Ӭ";s:4:"Ӭ";s:2:"ӭ";s:4:"ӭ";s:2:"Ӯ";s:4:"Ӯ";s:2:"ӯ";s:4:"ӯ";s:2:"Ӱ";s:4:"Ӱ";s:2:"ӱ";s:4:"ӱ";s:2:"Ӳ";s:4:"Ӳ";s:2:"ӳ";s:4:"ӳ";s:2:"Ӵ";s:4:"Ӵ";s:2:"ӵ";s:4:"ӵ";s:2:"Ӹ";s:4:"Ӹ";s:2:"ӹ";s:4:"ӹ";s:2:"և";s:4:"եւ";s:2:"آ";s:4:"آ";s:2:"أ";s:4:"أ";s:2:"ؤ";s:4:"ؤ";s:2:"إ";s:4:"إ";s:2:"ئ";s:4:"ئ";s:2:"ٵ";s:4:"اٴ";s:2:"ٶ";s:4:"وٴ";s:2:"ٷ";s:4:"ۇٴ";s:2:"ٸ";s:4:"يٴ";s:2:"ۀ";s:4:"ۀ";s:2:"ۂ";s:4:"ۂ";s:2:"ۓ";s:4:"ۓ";s:3:"ऩ";s:6:"ऩ";s:3:"ऱ";s:6:"ऱ";s:3:"ऴ";s:6:"ऴ";s:3:"क़";s:6:"क़";s:3:"ख़";s:6:"ख़";s:3:"ग़";s:6:"ग़";s:3:"ज़";s:6:"ज़";s:3:"ड़";s:6:"ड़";s:3:"ढ़";s:6:"ढ़";s:3:"फ़";s:6:"फ़";s:3:"य़";s:6:"य़";s:3:"ো";s:6:"ো";s:3:"ৌ";s:6:"ৌ";s:3:"ড়";s:6:"ড়";s:3:"ঢ়";s:6:"ঢ়";s:3:"য়";s:6:"য়";s:3:"ਲ਼";s:6:"ਲ਼";s:3:"ਸ਼";s:6:"ਸ਼";s:3:"ਖ਼";s:6:"ਖ਼";s:3:"ਗ਼";s:6:"ਗ਼";s:3:"ਜ਼";s:6:"ਜ਼";s:3:"ਫ਼";s:6:"ਫ਼";s:3:"ୈ";s:6:"ୈ";s:3:"ୋ";s:6:"ୋ";s:3:"ୌ";s:6:"ୌ";s:3:"ଡ଼";s:6:"ଡ଼";s:3:"ଢ଼";s:6:"ଢ଼";s:3:"ஔ";s:6:"ஔ";s:3:"ொ";s:6:"ொ";s:3:"ோ";s:6:"ோ";s:3:"ௌ";s:6:"ௌ";s:3:"ై";s:6:"ై";s:3:"ೀ";s:6:"ೀ";s:3:"ೇ";s:6:"ೇ";s:3:"ೈ";s:6:"ೈ";s:3:"ೊ";s:6:"ೊ";s:3:"ೋ";s:9:"ೋ";s:3:"ൊ";s:6:"ൊ";s:3:"ോ";s:6:"ോ";s:3:"ൌ";s:6:"ൌ";s:3:"ේ";s:6:"ේ";s:3:"ො";s:6:"ො";s:3:"ෝ";s:9:"ෝ";s:3:"ෞ";s:6:"ෞ";s:3:"ำ";s:6:"ํา";s:3:"ຳ";s:6:"ໍາ";s:3:"ໜ";s:6:"ຫນ";s:3:"ໝ";s:6:"ຫມ";s:3:"༌";s:3:"་";s:3:"གྷ";s:6:"གྷ";s:3:"ཌྷ";s:6:"ཌྷ";s:3:"དྷ";s:6:"དྷ";s:3:"བྷ";s:6:"བྷ";s:3:"ཛྷ";s:6:"ཛྷ";s:3:"ཀྵ";s:6:"ཀྵ";s:3:"ཱི";s:6:"ཱི";s:3:"ཱུ";s:6:"ཱུ";s:3:"ྲྀ";s:6:"ྲྀ";s:3:"ཷ";s:9:"ྲཱྀ";s:3:"ླྀ";s:6:"ླྀ";s:3:"ཹ";s:9:"ླཱྀ";s:3:"ཱྀ";s:6:"ཱྀ";s:3:"ྒྷ";s:6:"ྒྷ";s:3:"ྜྷ";s:6:"ྜྷ";s:3:"ྡྷ";s:6:"ྡྷ";s:3:"ྦྷ";s:6:"ྦྷ";s:3:"ྫྷ";s:6:"ྫྷ";s:3:"ྐྵ";s:6:"ྐྵ";s:3:"ဦ";s:6:"ဦ";s:3:"ჼ";s:3:"ნ";s:3:"ᬆ";s:6:"ᬆ";s:3:"ᬈ";s:6:"ᬈ";s:3:"ᬊ";s:6:"ᬊ";s:3:"ᬌ";s:6:"ᬌ";s:3:"ᬎ";s:6:"ᬎ";s:3:"ᬒ";s:6:"ᬒ";s:3:"ᬻ";s:6:"ᬻ";s:3:"ᬽ";s:6:"ᬽ";s:3:"ᭀ";s:6:"ᭀ";s:3:"ᭁ";s:6:"ᭁ";s:3:"ᭃ";s:6:"ᭃ";s:3:"ᴬ";s:1:"A";s:3:"ᴭ";s:2:"Æ";s:3:"ᴮ";s:1:"B";s:3:"ᴰ";s:1:"D";s:3:"ᴱ";s:1:"E";s:3:"ᴲ";s:2:"Ǝ";s:3:"ᴳ";s:1:"G";s:3:"ᴴ";s:1:"H";s:3:"ᴵ";s:1:"I";s:3:"ᴶ";s:1:"J";s:3:"ᴷ";s:1:"K";s:3:"ᴸ";s:1:"L";s:3:"ᴹ";s:1:"M";s:3:"ᴺ";s:1:"N";s:3:"ᴼ";s:1:"O";s:3:"ᴽ";s:2:"Ȣ";s:3:"ᴾ";s:1:"P";s:3:"ᴿ";s:1:"R";s:3:"ᵀ";s:1:"T";s:3:"ᵁ";s:1:"U";s:3:"ᵂ";s:1:"W";s:3:"ᵃ";s:1:"a";s:3:"ᵄ";s:2:"ɐ";s:3:"ᵅ";s:2:"ɑ";s:3:"ᵆ";s:3:"ᴂ";s:3:"ᵇ";s:1:"b";s:3:"ᵈ";s:1:"d";s:3:"ᵉ";s:1:"e";s:3:"ᵊ";s:2:"ə";s:3:"ᵋ";s:2:"ɛ";s:3:"ᵌ";s:2:"ɜ";s:3:"ᵍ";s:1:"g";s:3:"ᵏ";s:1:"k";s:3:"ᵐ";s:1:"m";s:3:"ᵑ";s:2:"ŋ";s:3:"ᵒ";s:1:"o";s:3:"ᵓ";s:2:"ɔ";s:3:"ᵔ";s:3:"ᴖ";s:3:"ᵕ";s:3:"ᴗ";s:3:"ᵖ";s:1:"p";s:3:"ᵗ";s:1:"t";s:3:"ᵘ";s:1:"u";s:3:"ᵙ";s:3:"ᴝ";s:3:"ᵚ";s:2:"ɯ";s:3:"ᵛ";s:1:"v";s:3:"ᵜ";s:3:"ᴥ";s:3:"ᵝ";s:2:"β";s:3:"ᵞ";s:2:"γ";s:3:"ᵟ";s:2:"δ";s:3:"ᵠ";s:2:"φ";s:3:"ᵡ";s:2:"χ";s:3:"ᵢ";s:1:"i";s:3:"ᵣ";s:1:"r";s:3:"ᵤ";s:1:"u";s:3:"ᵥ";s:1:"v";s:3:"ᵦ";s:2:"β";s:3:"ᵧ";s:2:"γ";s:3:"ᵨ";s:2:"ρ";s:3:"ᵩ";s:2:"φ";s:3:"ᵪ";s:2:"χ";s:3:"ᵸ";s:2:"н";s:3:"ᶛ";s:2:"ɒ";s:3:"ᶜ";s:1:"c";s:3:"ᶝ";s:2:"ɕ";s:3:"ᶞ";s:2:"ð";s:3:"ᶟ";s:2:"ɜ";s:3:"ᶠ";s:1:"f";s:3:"ᶡ";s:2:"ɟ";s:3:"ᶢ";s:2:"ɡ";s:3:"ᶣ";s:2:"ɥ";s:3:"ᶤ";s:2:"ɨ";s:3:"ᶥ";s:2:"ɩ";s:3:"ᶦ";s:2:"ɪ";s:3:"ᶧ";s:3:"ᵻ";s:3:"ᶨ";s:2:"ʝ";s:3:"ᶩ";s:2:"ɭ";s:3:"ᶪ";s:3:"ᶅ";s:3:"ᶫ";s:2:"ʟ";s:3:"ᶬ";s:2:"ɱ";s:3:"ᶭ";s:2:"ɰ";s:3:"ᶮ";s:2:"ɲ";s:3:"ᶯ";s:2:"ɳ";s:3:"ᶰ";s:2:"ɴ";s:3:"ᶱ";s:2:"ɵ";s:3:"ᶲ";s:2:"ɸ";s:3:"ᶳ";s:2:"ʂ";s:3:"ᶴ";s:2:"ʃ";s:3:"ᶵ";s:2:"ƫ";s:3:"ᶶ";s:2:"ʉ";s:3:"ᶷ";s:2:"ʊ";s:3:"ᶸ";s:3:"ᴜ";s:3:"ᶹ";s:2:"ʋ";s:3:"ᶺ";s:2:"ʌ";s:3:"ᶻ";s:1:"z";s:3:"ᶼ";s:2:"ʐ";s:3:"ᶽ";s:2:"ʑ";s:3:"ᶾ";s:2:"ʒ";s:3:"ᶿ";s:2:"θ";s:3:"Ḁ";s:3:"Ḁ";s:3:"ḁ";s:3:"ḁ";s:3:"Ḃ";s:3:"Ḃ";s:3:"ḃ";s:3:"ḃ";s:3:"Ḅ";s:3:"Ḅ";s:3:"ḅ";s:3:"ḅ";s:3:"Ḇ";s:3:"Ḇ";s:3:"ḇ";s:3:"ḇ";s:3:"Ḉ";s:5:"Ḉ";s:3:"ḉ";s:5:"ḉ";s:3:"Ḋ";s:3:"Ḋ";s:3:"ḋ";s:3:"ḋ";s:3:"Ḍ";s:3:"Ḍ";s:3:"ḍ";s:3:"ḍ";s:3:"Ḏ";s:3:"Ḏ";s:3:"ḏ";s:3:"ḏ";s:3:"Ḑ";s:3:"Ḑ";s:3:"ḑ";s:3:"ḑ";s:3:"Ḓ";s:3:"Ḓ";s:3:"ḓ";s:3:"ḓ";s:3:"Ḕ";s:5:"Ḕ";s:3:"ḕ";s:5:"ḕ";s:3:"Ḗ";s:5:"Ḗ";s:3:"ḗ";s:5:"ḗ";s:3:"Ḙ";s:3:"Ḙ";s:3:"ḙ";s:3:"ḙ";s:3:"Ḛ";s:3:"Ḛ";s:3:"ḛ";s:3:"ḛ";s:3:"Ḝ";s:5:"Ḝ";s:3:"ḝ";s:5:"ḝ";s:3:"Ḟ";s:3:"Ḟ";s:3:"ḟ";s:3:"ḟ";s:3:"Ḡ";s:3:"Ḡ";s:3:"ḡ";s:3:"ḡ";s:3:"Ḣ";s:3:"Ḣ";s:3:"ḣ";s:3:"ḣ";s:3:"Ḥ";s:3:"Ḥ";s:3:"ḥ";s:3:"ḥ";s:3:"Ḧ";s:3:"Ḧ";s:3:"ḧ";s:3:"ḧ";s:3:"Ḩ";s:3:"Ḩ";s:3:"ḩ";s:3:"ḩ";s:3:"Ḫ";s:3:"Ḫ";s:3:"ḫ";s:3:"ḫ";s:3:"Ḭ";s:3:"Ḭ";s:3:"ḭ";s:3:"ḭ";s:3:"Ḯ";s:5:"Ḯ";s:3:"ḯ";s:5:"ḯ";s:3:"Ḱ";s:3:"Ḱ";s:3:"ḱ";s:3:"ḱ";s:3:"Ḳ";s:3:"Ḳ";s:3:"ḳ";s:3:"ḳ";s:3:"Ḵ";s:3:"Ḵ";s:3:"ḵ";s:3:"ḵ";s:3:"Ḷ";s:3:"Ḷ";s:3:"ḷ";s:3:"ḷ";s:3:"Ḹ";s:5:"Ḹ";s:3:"ḹ";s:5:"ḹ";s:3:"Ḻ";s:3:"Ḻ";s:3:"ḻ";s:3:"ḻ";s:3:"Ḽ";s:3:"Ḽ";s:3:"ḽ";s:3:"ḽ";s:3:"Ḿ";s:3:"Ḿ";s:3:"ḿ";s:3:"ḿ";s:3:"Ṁ";s:3:"Ṁ";s:3:"ṁ";s:3:"ṁ";s:3:"Ṃ";s:3:"Ṃ";s:3:"ṃ";s:3:"ṃ";s:3:"Ṅ";s:3:"Ṅ";s:3:"ṅ";s:3:"ṅ";s:3:"Ṇ";s:3:"Ṇ";s:3:"ṇ";s:3:"ṇ";s:3:"Ṉ";s:3:"Ṉ";s:3:"ṉ";s:3:"ṉ";s:3:"Ṋ";s:3:"Ṋ";s:3:"ṋ";s:3:"ṋ";s:3:"Ṍ";s:5:"Ṍ";s:3:"ṍ";s:5:"ṍ";s:3:"Ṏ";s:5:"Ṏ";s:3:"ṏ";s:5:"ṏ";s:3:"Ṑ";s:5:"Ṑ";s:3:"ṑ";s:5:"ṑ";s:3:"Ṓ";s:5:"Ṓ";s:3:"ṓ";s:5:"ṓ";s:3:"Ṕ";s:3:"Ṕ";s:3:"ṕ";s:3:"ṕ";s:3:"Ṗ";s:3:"Ṗ";s:3:"ṗ";s:3:"ṗ";s:3:"Ṙ";s:3:"Ṙ";s:3:"ṙ";s:3:"ṙ";s:3:"Ṛ";s:3:"Ṛ";s:3:"ṛ";s:3:"ṛ";s:3:"Ṝ";s:5:"Ṝ";s:3:"ṝ";s:5:"ṝ";s:3:"Ṟ";s:3:"Ṟ";s:3:"ṟ";s:3:"ṟ";s:3:"Ṡ";s:3:"Ṡ";s:3:"ṡ";s:3:"ṡ";s:3:"Ṣ";s:3:"Ṣ";s:3:"ṣ";s:3:"ṣ";s:3:"Ṥ";s:5:"Ṥ";s:3:"ṥ";s:5:"ṥ";s:3:"Ṧ";s:5:"Ṧ";s:3:"ṧ";s:5:"ṧ";s:3:"Ṩ";s:5:"Ṩ";s:3:"ṩ";s:5:"ṩ";s:3:"Ṫ";s:3:"Ṫ";s:3:"ṫ";s:3:"ṫ";s:3:"Ṭ";s:3:"Ṭ";s:3:"ṭ";s:3:"ṭ";s:3:"Ṯ";s:3:"Ṯ";s:3:"ṯ";s:3:"ṯ";s:3:"Ṱ";s:3:"Ṱ";s:3:"ṱ";s:3:"ṱ";s:3:"Ṳ";s:3:"Ṳ";s:3:"ṳ";s:3:"ṳ";s:3:"Ṵ";s:3:"Ṵ";s:3:"ṵ";s:3:"ṵ";s:3:"Ṷ";s:3:"Ṷ";s:3:"ṷ";s:3:"ṷ";s:3:"Ṹ";s:5:"Ṹ";s:3:"ṹ";s:5:"ṹ";s:3:"Ṻ";s:5:"Ṻ";s:3:"ṻ";s:5:"ṻ";s:3:"Ṽ";s:3:"Ṽ";s:3:"ṽ";s:3:"ṽ";s:3:"Ṿ";s:3:"Ṿ";s:3:"ṿ";s:3:"ṿ";s:3:"Ẁ";s:3:"Ẁ";s:3:"ẁ";s:3:"ẁ";s:3:"Ẃ";s:3:"Ẃ";s:3:"ẃ";s:3:"ẃ";s:3:"Ẅ";s:3:"Ẅ";s:3:"ẅ";s:3:"ẅ";s:3:"Ẇ";s:3:"Ẇ";s:3:"ẇ";s:3:"ẇ";s:3:"Ẉ";s:3:"Ẉ";s:3:"ẉ";s:3:"ẉ";s:3:"Ẋ";s:3:"Ẋ";s:3:"ẋ";s:3:"ẋ";s:3:"Ẍ";s:3:"Ẍ";s:3:"ẍ";s:3:"ẍ";s:3:"Ẏ";s:3:"Ẏ";s:3:"ẏ";s:3:"ẏ";s:3:"Ẑ";s:3:"Ẑ";s:3:"ẑ";s:3:"ẑ";s:3:"Ẓ";s:3:"Ẓ";s:3:"ẓ";s:3:"ẓ";s:3:"Ẕ";s:3:"Ẕ";s:3:"ẕ";s:3:"ẕ";s:3:"ẖ";s:3:"ẖ";s:3:"ẗ";s:3:"ẗ";s:3:"ẘ";s:3:"ẘ";s:3:"ẙ";s:3:"ẙ";s:3:"ẚ";s:3:"aʾ";s:3:"ẛ";s:3:"ṡ";s:3:"Ạ";s:3:"Ạ";s:3:"ạ";s:3:"ạ";s:3:"Ả";s:3:"Ả";s:3:"ả";s:3:"ả";s:3:"Ấ";s:5:"Ấ";s:3:"ấ";s:5:"ấ";s:3:"Ầ";s:5:"Ầ";s:3:"ầ";s:5:"ầ";s:3:"Ẩ";s:5:"Ẩ";s:3:"ẩ";s:5:"ẩ";s:3:"Ẫ";s:5:"Ẫ";s:3:"ẫ";s:5:"ẫ";s:3:"Ậ";s:5:"Ậ";s:3:"ậ";s:5:"ậ";s:3:"Ắ";s:5:"Ắ";s:3:"ắ";s:5:"ắ";s:3:"Ằ";s:5:"Ằ";s:3:"ằ";s:5:"ằ";s:3:"Ẳ";s:5:"Ẳ";s:3:"ẳ";s:5:"ẳ";s:3:"Ẵ";s:5:"Ẵ";s:3:"ẵ";s:5:"ẵ";s:3:"Ặ";s:5:"Ặ";s:3:"ặ";s:5:"ặ";s:3:"Ẹ";s:3:"Ẹ";s:3:"ẹ";s:3:"ẹ";s:3:"Ẻ";s:3:"Ẻ";s:3:"ẻ";s:3:"ẻ";s:3:"Ẽ";s:3:"Ẽ";s:3:"ẽ";s:3:"ẽ";s:3:"Ế";s:5:"Ế";s:3:"ế";s:5:"ế";s:3:"Ề";s:5:"Ề";s:3:"ề";s:5:"ề";s:3:"Ể";s:5:"Ể";s:3:"ể";s:5:"ể";s:3:"Ễ";s:5:"Ễ";s:3:"ễ";s:5:"ễ";s:3:"Ệ";s:5:"Ệ";s:3:"ệ";s:5:"ệ";s:3:"Ỉ";s:3:"Ỉ";s:3:"ỉ";s:3:"ỉ";s:3:"Ị";s:3:"Ị";s:3:"ị";s:3:"ị";s:3:"Ọ";s:3:"Ọ";s:3:"ọ";s:3:"ọ";s:3:"Ỏ";s:3:"Ỏ";s:3:"ỏ";s:3:"ỏ";s:3:"Ố";s:5:"Ố";s:3:"ố";s:5:"ố";s:3:"Ồ";s:5:"Ồ";s:3:"ồ";s:5:"ồ";s:3:"Ổ";s:5:"Ổ";s:3:"ổ";s:5:"ổ";s:3:"Ỗ";s:5:"Ỗ";s:3:"ỗ";s:5:"ỗ";s:3:"Ộ";s:5:"Ộ";s:3:"ộ";s:5:"ộ";s:3:"Ớ";s:5:"Ớ";s:3:"ớ";s:5:"ớ";s:3:"Ờ";s:5:"Ờ";s:3:"ờ";s:5:"ờ";s:3:"Ở";s:5:"Ở";s:3:"ở";s:5:"ở";s:3:"Ỡ";s:5:"Ỡ";s:3:"ỡ";s:5:"ỡ";s:3:"Ợ";s:5:"Ợ";s:3:"ợ";s:5:"ợ";s:3:"Ụ";s:3:"Ụ";s:3:"ụ";s:3:"ụ";s:3:"Ủ";s:3:"Ủ";s:3:"ủ";s:3:"ủ";s:3:"Ứ";s:5:"Ứ";s:3:"ứ";s:5:"ứ";s:3:"Ừ";s:5:"Ừ";s:3:"ừ";s:5:"ừ";s:3:"Ử";s:5:"Ử";s:3:"ử";s:5:"ử";s:3:"Ữ";s:5:"Ữ";s:3:"ữ";s:5:"ữ";s:3:"Ự";s:5:"Ự";s:3:"ự";s:5:"ự";s:3:"Ỳ";s:3:"Ỳ";s:3:"ỳ";s:3:"ỳ";s:3:"Ỵ";s:3:"Ỵ";s:3:"ỵ";s:3:"ỵ";s:3:"Ỷ";s:3:"Ỷ";s:3:"ỷ";s:3:"ỷ";s:3:"Ỹ";s:3:"Ỹ";s:3:"ỹ";s:3:"ỹ";s:3:"ἀ";s:4:"ἀ";s:3:"ἁ";s:4:"ἁ";s:3:"ἂ";s:6:"ἂ";s:3:"ἃ";s:6:"ἃ";s:3:"ἄ";s:6:"ἄ";s:3:"ἅ";s:6:"ἅ";s:3:"ἆ";s:6:"ἆ";s:3:"ἇ";s:6:"ἇ";s:3:"Ἀ";s:4:"Ἀ";s:3:"Ἁ";s:4:"Ἁ";s:3:"Ἂ";s:6:"Ἂ";s:3:"Ἃ";s:6:"Ἃ";s:3:"Ἄ";s:6:"Ἄ";s:3:"Ἅ";s:6:"Ἅ";s:3:"Ἆ";s:6:"Ἆ";s:3:"Ἇ";s:6:"Ἇ";s:3:"ἐ";s:4:"ἐ";s:3:"ἑ";s:4:"ἑ";s:3:"ἒ";s:6:"ἒ";s:3:"ἓ";s:6:"ἓ";s:3:"ἔ";s:6:"ἔ";s:3:"ἕ";s:6:"ἕ";s:3:"Ἐ";s:4:"Ἐ";s:3:"Ἑ";s:4:"Ἑ";s:3:"Ἒ";s:6:"Ἒ";s:3:"Ἓ";s:6:"Ἓ";s:3:"Ἔ";s:6:"Ἔ";s:3:"Ἕ";s:6:"Ἕ";s:3:"ἠ";s:4:"ἠ";s:3:"ἡ";s:4:"ἡ";s:3:"ἢ";s:6:"ἢ";s:3:"ἣ";s:6:"ἣ";s:3:"ἤ";s:6:"ἤ";s:3:"ἥ";s:6:"ἥ";s:3:"ἦ";s:6:"ἦ";s:3:"ἧ";s:6:"ἧ";s:3:"Ἠ";s:4:"Ἠ";s:3:"Ἡ";s:4:"Ἡ";s:3:"Ἢ";s:6:"Ἢ";s:3:"Ἣ";s:6:"Ἣ";s:3:"Ἤ";s:6:"Ἤ";s:3:"Ἥ";s:6:"Ἥ";s:3:"Ἦ";s:6:"Ἦ";s:3:"Ἧ";s:6:"Ἧ";s:3:"ἰ";s:4:"ἰ";s:3:"ἱ";s:4:"ἱ";s:3:"ἲ";s:6:"ἲ";s:3:"ἳ";s:6:"ἳ";s:3:"ἴ";s:6:"ἴ";s:3:"ἵ";s:6:"ἵ";s:3:"ἶ";s:6:"ἶ";s:3:"ἷ";s:6:"ἷ";s:3:"Ἰ";s:4:"Ἰ";s:3:"Ἱ";s:4:"Ἱ";s:3:"Ἲ";s:6:"Ἲ";s:3:"Ἳ";s:6:"Ἳ";s:3:"Ἴ";s:6:"Ἴ";s:3:"Ἵ";s:6:"Ἵ";s:3:"Ἶ";s:6:"Ἶ";s:3:"Ἷ";s:6:"Ἷ";s:3:"ὀ";s:4:"ὀ";s:3:"ὁ";s:4:"ὁ";s:3:"ὂ";s:6:"ὂ";s:3:"ὃ";s:6:"ὃ";s:3:"ὄ";s:6:"ὄ";s:3:"ὅ";s:6:"ὅ";s:3:"Ὀ";s:4:"Ὀ";s:3:"Ὁ";s:4:"Ὁ";s:3:"Ὂ";s:6:"Ὂ";s:3:"Ὃ";s:6:"Ὃ";s:3:"Ὄ";s:6:"Ὄ";s:3:"Ὅ";s:6:"Ὅ";s:3:"ὐ";s:4:"ὐ";s:3:"ὑ";s:4:"ὑ";s:3:"ὒ";s:6:"ὒ";s:3:"ὓ";s:6:"ὓ";s:3:"ὔ";s:6:"ὔ";s:3:"ὕ";s:6:"ὕ";s:3:"ὖ";s:6:"ὖ";s:3:"ὗ";s:6:"ὗ";s:3:"Ὑ";s:4:"Ὑ";s:3:"Ὓ";s:6:"Ὓ";s:3:"Ὕ";s:6:"Ὕ";s:3:"Ὗ";s:6:"Ὗ";s:3:"ὠ";s:4:"ὠ";s:3:"ὡ";s:4:"ὡ";s:3:"ὢ";s:6:"ὢ";s:3:"ὣ";s:6:"ὣ";s:3:"ὤ";s:6:"ὤ";s:3:"ὥ";s:6:"ὥ";s:3:"ὦ";s:6:"ὦ";s:3:"ὧ";s:6:"ὧ";s:3:"Ὠ";s:4:"Ὠ";s:3:"Ὡ";s:4:"Ὡ";s:3:"Ὢ";s:6:"Ὢ";s:3:"Ὣ";s:6:"Ὣ";s:3:"Ὤ";s:6:"Ὤ";s:3:"Ὥ";s:6:"Ὥ";s:3:"Ὦ";s:6:"Ὦ";s:3:"Ὧ";s:6:"Ὧ";s:3:"ὰ";s:4:"ὰ";s:3:"ά";s:4:"ά";s:3:"ὲ";s:4:"ὲ";s:3:"έ";s:4:"έ";s:3:"ὴ";s:4:"ὴ";s:3:"ή";s:4:"ή";s:3:"ὶ";s:4:"ὶ";s:3:"ί";s:4:"ί";s:3:"ὸ";s:4:"ὸ";s:3:"ό";s:4:"ό";s:3:"ὺ";s:4:"ὺ";s:3:"ύ";s:4:"ύ";s:3:"ὼ";s:4:"ὼ";s:3:"ώ";s:4:"ώ";s:3:"ᾀ";s:6:"ᾀ";s:3:"ᾁ";s:6:"ᾁ";s:3:"ᾂ";s:8:"ᾂ";s:3:"ᾃ";s:8:"ᾃ";s:3:"ᾄ";s:8:"ᾄ";s:3:"ᾅ";s:8:"ᾅ";s:3:"ᾆ";s:8:"ᾆ";s:3:"ᾇ";s:8:"ᾇ";s:3:"ᾈ";s:6:"ᾈ";s:3:"ᾉ";s:6:"ᾉ";s:3:"ᾊ";s:8:"ᾊ";s:3:"ᾋ";s:8:"ᾋ";s:3:"ᾌ";s:8:"ᾌ";s:3:"ᾍ";s:8:"ᾍ";s:3:"ᾎ";s:8:"ᾎ";s:3:"ᾏ";s:8:"ᾏ";s:3:"ᾐ";s:6:"ᾐ";s:3:"ᾑ";s:6:"ᾑ";s:3:"ᾒ";s:8:"ᾒ";s:3:"ᾓ";s:8:"ᾓ";s:3:"ᾔ";s:8:"ᾔ";s:3:"ᾕ";s:8:"ᾕ";s:3:"ᾖ";s:8:"ᾖ";s:3:"ᾗ";s:8:"ᾗ";s:3:"ᾘ";s:6:"ᾘ";s:3:"ᾙ";s:6:"ᾙ";s:3:"ᾚ";s:8:"ᾚ";s:3:"ᾛ";s:8:"ᾛ";s:3:"ᾜ";s:8:"ᾜ";s:3:"ᾝ";s:8:"ᾝ";s:3:"ᾞ";s:8:"ᾞ";s:3:"ᾟ";s:8:"ᾟ";s:3:"ᾠ";s:6:"ᾠ";s:3:"ᾡ";s:6:"ᾡ";s:3:"ᾢ";s:8:"ᾢ";s:3:"ᾣ";s:8:"ᾣ";s:3:"ᾤ";s:8:"ᾤ";s:3:"ᾥ";s:8:"ᾥ";s:3:"ᾦ";s:8:"ᾦ";s:3:"ᾧ";s:8:"ᾧ";s:3:"ᾨ";s:6:"ᾨ";s:3:"ᾩ";s:6:"ᾩ";s:3:"ᾪ";s:8:"ᾪ";s:3:"ᾫ";s:8:"ᾫ";s:3:"ᾬ";s:8:"ᾬ";s:3:"ᾭ";s:8:"ᾭ";s:3:"ᾮ";s:8:"ᾮ";s:3:"ᾯ";s:8:"ᾯ";s:3:"ᾰ";s:4:"ᾰ";s:3:"ᾱ";s:4:"ᾱ";s:3:"ᾲ";s:6:"ᾲ";s:3:"ᾳ";s:4:"ᾳ";s:3:"ᾴ";s:6:"ᾴ";s:3:"ᾶ";s:4:"ᾶ";s:3:"ᾷ";s:6:"ᾷ";s:3:"Ᾰ";s:4:"Ᾰ";s:3:"Ᾱ";s:4:"Ᾱ";s:3:"Ὰ";s:4:"Ὰ";s:3:"Ά";s:4:"Ά";s:3:"ᾼ";s:4:"ᾼ";s:3:"᾽";s:3:" ̓";s:3:"ι";s:2:"ι";s:3:"᾿";s:3:" ̓";s:3:"῀";s:3:" ͂";s:3:"῁";s:5:" ̈͂";s:3:"ῂ";s:6:"ῂ";s:3:"ῃ";s:4:"ῃ";s:3:"ῄ";s:6:"ῄ";s:3:"ῆ";s:4:"ῆ";s:3:"ῇ";s:6:"ῇ";s:3:"Ὲ";s:4:"Ὲ";s:3:"Έ";s:4:"Έ";s:3:"Ὴ";s:4:"Ὴ";s:3:"Ή";s:4:"Ή";s:3:"ῌ";s:4:"ῌ";s:3:"῍";s:5:" ̓̀";s:3:"῎";s:5:" ̓́";s:3:"῏";s:5:" ̓͂";s:3:"ῐ";s:4:"ῐ";s:3:"ῑ";s:4:"ῑ";s:3:"ῒ";s:6:"ῒ";s:3:"ΐ";s:6:"ΐ";s:3:"ῖ";s:4:"ῖ";s:3:"ῗ";s:6:"ῗ";s:3:"Ῐ";s:4:"Ῐ";s:3:"Ῑ";s:4:"Ῑ";s:3:"Ὶ";s:4:"Ὶ";s:3:"Ί";s:4:"Ί";s:3:"῝";s:5:" ̔̀";s:3:"῞";s:5:" ̔́";s:3:"῟";s:5:" ̔͂";s:3:"ῠ";s:4:"ῠ";s:3:"ῡ";s:4:"ῡ";s:3:"ῢ";s:6:"ῢ";s:3:"ΰ";s:6:"ΰ";s:3:"ῤ";s:4:"ῤ";s:3:"ῥ";s:4:"ῥ";s:3:"ῦ";s:4:"ῦ";s:3:"ῧ";s:6:"ῧ";s:3:"Ῠ";s:4:"Ῠ";s:3:"Ῡ";s:4:"Ῡ";s:3:"Ὺ";s:4:"Ὺ";s:3:"Ύ";s:4:"Ύ";s:3:"Ῥ";s:4:"Ῥ";s:3:"῭";s:5:" ̈̀";s:3:"΅";s:5:" ̈́";s:3:"`";s:1:"`";s:3:"ῲ";s:6:"ῲ";s:3:"ῳ";s:4:"ῳ";s:3:"ῴ";s:6:"ῴ";s:3:"ῶ";s:4:"ῶ";s:3:"ῷ";s:6:"ῷ";s:3:"Ὸ";s:4:"Ὸ";s:3:"Ό";s:4:"Ό";s:3:"Ὼ";s:4:"Ὼ";s:3:"Ώ";s:4:"Ώ";s:3:"ῼ";s:4:"ῼ";s:3:"´";s:3:" ́";s:3:"῾";s:3:" ̔";s:3:" ";s:1:" ";s:3:" ";s:1:" ";s:3:" ";s:1:" ";s:3:" ";s:1:" ";s:3:" ";s:1:" ";s:3:" ";s:1:" ";s:3:" ";s:1:" ";s:3:" ";s:1:" ";s:3:" ";s:1:" ";s:3:" ";s:1:" ";s:3:" ";s:1:" ";s:3:"‑";s:3:"‐";s:3:"‗";s:3:" ̳";s:3:"․";s:1:".";s:3:"‥";s:2:"..";s:3:"…";s:3:"...";s:3:" ";s:1:" ";s:3:"″";s:6:"′′";s:3:"‴";s:9:"′′′";s:3:"‶";s:6:"‵‵";s:3:"‷";s:9:"‵‵‵";s:3:"‼";s:2:"!!";s:3:"‾";s:3:" ̅";s:3:"⁇";s:2:"??";s:3:"⁈";s:2:"?!";s:3:"⁉";s:2:"!?";s:3:"⁗";s:12:"′′′′";s:3:" ";s:1:" ";s:3:"⁰";s:1:"0";s:3:"ⁱ";s:1:"i";s:3:"⁴";s:1:"4";s:3:"⁵";s:1:"5";s:3:"⁶";s:1:"6";s:3:"⁷";s:1:"7";s:3:"⁸";s:1:"8";s:3:"⁹";s:1:"9";s:3:"⁺";s:1:"+";s:3:"⁻";s:3:"−";s:3:"⁼";s:1:"=";s:3:"⁽";s:1:"(";s:3:"⁾";s:1:")";s:3:"ⁿ";s:1:"n";s:3:"₀";s:1:"0";s:3:"₁";s:1:"1";s:3:"₂";s:1:"2";s:3:"₃";s:1:"3";s:3:"₄";s:1:"4";s:3:"₅";s:1:"5";s:3:"₆";s:1:"6";s:3:"₇";s:1:"7";s:3:"₈";s:1:"8";s:3:"₉";s:1:"9";s:3:"₊";s:1:"+";s:3:"₋";s:3:"−";s:3:"₌";s:1:"=";s:3:"₍";s:1:"(";s:3:"₎";s:1:")";s:3:"ₐ";s:1:"a";s:3:"ₑ";s:1:"e";s:3:"ₒ";s:1:"o";s:3:"ₓ";s:1:"x";s:3:"ₔ";s:2:"ə";s:3:"₨";s:2:"Rs";s:3:"℀";s:3:"a/c";s:3:"℁";s:3:"a/s";s:3:"ℂ";s:1:"C";s:3:"℃";s:3:"°C";s:3:"℅";s:3:"c/o";s:3:"℆";s:3:"c/u";s:3:"ℇ";s:2:"Ɛ";s:3:"℉";s:3:"°F";s:3:"ℊ";s:1:"g";s:3:"ℋ";s:1:"H";s:3:"ℌ";s:1:"H";s:3:"ℍ";s:1:"H";s:3:"ℎ";s:1:"h";s:3:"ℏ";s:2:"ħ";s:3:"ℐ";s:1:"I";s:3:"ℑ";s:1:"I";s:3:"ℒ";s:1:"L";s:3:"ℓ";s:1:"l";s:3:"ℕ";s:1:"N";s:3:"№";s:2:"No";s:3:"ℙ";s:1:"P";s:3:"ℚ";s:1:"Q";s:3:"ℛ";s:1:"R";s:3:"ℜ";s:1:"R";s:3:"ℝ";s:1:"R";s:3:"℠";s:2:"SM";s:3:"℡";s:3:"TEL";s:3:"™";s:2:"TM";s:3:"ℤ";s:1:"Z";s:3:"Ω";s:2:"Ω";s:3:"ℨ";s:1:"Z";s:3:"K";s:1:"K";s:3:"Å";s:3:"Å";s:3:"ℬ";s:1:"B";s:3:"ℭ";s:1:"C";s:3:"ℯ";s:1:"e";s:3:"ℰ";s:1:"E";s:3:"ℱ";s:1:"F";s:3:"ℳ";s:1:"M";s:3:"ℴ";s:1:"o";s:3:"ℵ";s:2:"א";s:3:"ℶ";s:2:"ב";s:3:"ℷ";s:2:"ג";s:3:"ℸ";s:2:"ד";s:3:"ℹ";s:1:"i";s:3:"℻";s:3:"FAX";s:3:"ℼ";s:2:"π";s:3:"ℽ";s:2:"γ";s:3:"ℾ";s:2:"Γ";s:3:"ℿ";s:2:"Π";s:3:"⅀";s:3:"∑";s:3:"ⅅ";s:1:"D";s:3:"ⅆ";s:1:"d";s:3:"ⅇ";s:1:"e";s:3:"ⅈ";s:1:"i";s:3:"ⅉ";s:1:"j";s:3:"⅓";s:5:"1⁄3";s:3:"⅔";s:5:"2⁄3";s:3:"⅕";s:5:"1⁄5";s:3:"⅖";s:5:"2⁄5";s:3:"⅗";s:5:"3⁄5";s:3:"⅘";s:5:"4⁄5";s:3:"⅙";s:5:"1⁄6";s:3:"⅚";s:5:"5⁄6";s:3:"⅛";s:5:"1⁄8";s:3:"⅜";s:5:"3⁄8";s:3:"⅝";s:5:"5⁄8";s:3:"⅞";s:5:"7⁄8";s:3:"⅟";s:4:"1⁄";s:3:"Ⅰ";s:1:"I";s:3:"Ⅱ";s:2:"II";s:3:"Ⅲ";s:3:"III";s:3:"Ⅳ";s:2:"IV";s:3:"Ⅴ";s:1:"V";s:3:"Ⅵ";s:2:"VI";s:3:"Ⅶ";s:3:"VII";s:3:"Ⅷ";s:4:"VIII";s:3:"Ⅸ";s:2:"IX";s:3:"Ⅹ";s:1:"X";s:3:"Ⅺ";s:2:"XI";s:3:"Ⅻ";s:3:"XII";s:3:"Ⅼ";s:1:"L";s:3:"Ⅽ";s:1:"C";s:3:"Ⅾ";s:1:"D";s:3:"Ⅿ";s:1:"M";s:3:"ⅰ";s:1:"i";s:3:"ⅱ";s:2:"ii";s:3:"ⅲ";s:3:"iii";s:3:"ⅳ";s:2:"iv";s:3:"ⅴ";s:1:"v";s:3:"ⅵ";s:2:"vi";s:3:"ⅶ";s:3:"vii";s:3:"ⅷ";s:4:"viii";s:3:"ⅸ";s:2:"ix";s:3:"ⅹ";s:1:"x";s:3:"ⅺ";s:2:"xi";s:3:"ⅻ";s:3:"xii";s:3:"ⅼ";s:1:"l";s:3:"ⅽ";s:1:"c";s:3:"ⅾ";s:1:"d";s:3:"ⅿ";s:1:"m";s:3:"↚";s:5:"↚";s:3:"↛";s:5:"↛";s:3:"↮";s:5:"↮";s:3:"⇍";s:5:"⇍";s:3:"⇎";s:5:"⇎";s:3:"⇏";s:5:"⇏";s:3:"∄";s:5:"∄";s:3:"∉";s:5:"∉";s:3:"∌";s:5:"∌";s:3:"∤";s:5:"∤";s:3:"∦";s:5:"∦";s:3:"∬";s:6:"∫∫";s:3:"∭";s:9:"∫∫∫";s:3:"∯";s:6:"∮∮";s:3:"∰";s:9:"∮∮∮";s:3:"≁";s:5:"≁";s:3:"≄";s:5:"≄";s:3:"≇";s:5:"≇";s:3:"≉";s:5:"≉";s:3:"≠";s:3:"≠";s:3:"≢";s:5:"≢";s:3:"≭";s:5:"≭";s:3:"≮";s:3:"≮";s:3:"≯";s:3:"≯";s:3:"≰";s:5:"≰";s:3:"≱";s:5:"≱";s:3:"≴";s:5:"≴";s:3:"≵";s:5:"≵";s:3:"≸";s:5:"≸";s:3:"≹";s:5:"≹";s:3:"⊀";s:5:"⊀";s:3:"⊁";s:5:"⊁";s:3:"⊄";s:5:"⊄";s:3:"⊅";s:5:"⊅";s:3:"⊈";s:5:"⊈";s:3:"⊉";s:5:"⊉";s:3:"⊬";s:5:"⊬";s:3:"⊭";s:5:"⊭";s:3:"⊮";s:5:"⊮";s:3:"⊯";s:5:"⊯";s:3:"⋠";s:5:"⋠";s:3:"⋡";s:5:"⋡";s:3:"⋢";s:5:"⋢";s:3:"⋣";s:5:"⋣";s:3:"⋪";s:5:"⋪";s:3:"⋫";s:5:"⋫";s:3:"⋬";s:5:"⋬";s:3:"⋭";s:5:"⋭";s:3:"〈";s:3:"〈";s:3:"〉";s:3:"〉";s:3:"①";s:1:"1";s:3:"②";s:1:"2";s:3:"③";s:1:"3";s:3:"④";s:1:"4";s:3:"⑤";s:1:"5";s:3:"⑥";s:1:"6";s:3:"⑦";s:1:"7";s:3:"⑧";s:1:"8";s:3:"⑨";s:1:"9";s:3:"⑩";s:2:"10";s:3:"⑪";s:2:"11";s:3:"⑫";s:2:"12";s:3:"⑬";s:2:"13";s:3:"⑭";s:2:"14";s:3:"⑮";s:2:"15";s:3:"⑯";s:2:"16";s:3:"⑰";s:2:"17";s:3:"⑱";s:2:"18";s:3:"⑲";s:2:"19";s:3:"⑳";s:2:"20";s:3:"⑴";s:3:"(1)";s:3:"⑵";s:3:"(2)";s:3:"⑶";s:3:"(3)";s:3:"⑷";s:3:"(4)";s:3:"⑸";s:3:"(5)";s:3:"⑹";s:3:"(6)";s:3:"⑺";s:3:"(7)";s:3:"⑻";s:3:"(8)";s:3:"⑼";s:3:"(9)";s:3:"⑽";s:4:"(10)";s:3:"⑾";s:4:"(11)";s:3:"⑿";s:4:"(12)";s:3:"⒀";s:4:"(13)";s:3:"⒁";s:4:"(14)";s:3:"⒂";s:4:"(15)";s:3:"⒃";s:4:"(16)";s:3:"⒄";s:4:"(17)";s:3:"⒅";s:4:"(18)";s:3:"⒆";s:4:"(19)";s:3:"⒇";s:4:"(20)";s:3:"⒈";s:2:"1.";s:3:"⒉";s:2:"2.";s:3:"⒊";s:2:"3.";s:3:"⒋";s:2:"4.";s:3:"⒌";s:2:"5.";s:3:"⒍";s:2:"6.";s:3:"⒎";s:2:"7.";s:3:"⒏";s:2:"8.";s:3:"⒐";s:2:"9.";s:3:"⒑";s:3:"10.";s:3:"⒒";s:3:"11.";s:3:"⒓";s:3:"12.";s:3:"⒔";s:3:"13.";s:3:"⒕";s:3:"14.";s:3:"⒖";s:3:"15.";s:3:"⒗";s:3:"16.";s:3:"⒘";s:3:"17.";s:3:"⒙";s:3:"18.";s:3:"⒚";s:3:"19.";s:3:"⒛";s:3:"20.";s:3:"⒜";s:3:"(a)";s:3:"⒝";s:3:"(b)";s:3:"⒞";s:3:"(c)";s:3:"⒟";s:3:"(d)";s:3:"⒠";s:3:"(e)";s:3:"⒡";s:3:"(f)";s:3:"⒢";s:3:"(g)";s:3:"⒣";s:3:"(h)";s:3:"⒤";s:3:"(i)";s:3:"⒥";s:3:"(j)";s:3:"⒦";s:3:"(k)";s:3:"⒧";s:3:"(l)";s:3:"⒨";s:3:"(m)";s:3:"⒩";s:3:"(n)";s:3:"⒪";s:3:"(o)";s:3:"⒫";s:3:"(p)";s:3:"⒬";s:3:"(q)";s:3:"⒭";s:3:"(r)";s:3:"⒮";s:3:"(s)";s:3:"⒯";s:3:"(t)";s:3:"⒰";s:3:"(u)";s:3:"⒱";s:3:"(v)";s:3:"⒲";s:3:"(w)";s:3:"⒳";s:3:"(x)";s:3:"⒴";s:3:"(y)";s:3:"⒵";s:3:"(z)";s:3:"Ⓐ";s:1:"A";s:3:"Ⓑ";s:1:"B";s:3:"Ⓒ";s:1:"C";s:3:"Ⓓ";s:1:"D";s:3:"Ⓔ";s:1:"E";s:3:"Ⓕ";s:1:"F";s:3:"Ⓖ";s:1:"G";s:3:"Ⓗ";s:1:"H";s:3:"Ⓘ";s:1:"I";s:3:"Ⓙ";s:1:"J";s:3:"Ⓚ";s:1:"K";s:3:"Ⓛ";s:1:"L";s:3:"Ⓜ";s:1:"M";s:3:"Ⓝ";s:1:"N";s:3:"Ⓞ";s:1:"O";s:3:"Ⓟ";s:1:"P";s:3:"Ⓠ";s:1:"Q";s:3:"Ⓡ";s:1:"R";s:3:"Ⓢ";s:1:"S";s:3:"Ⓣ";s:1:"T";s:3:"Ⓤ";s:1:"U";s:3:"Ⓥ";s:1:"V";s:3:"Ⓦ";s:1:"W";s:3:"Ⓧ";s:1:"X";s:3:"Ⓨ";s:1:"Y";s:3:"Ⓩ";s:1:"Z";s:3:"ⓐ";s:1:"a";s:3:"ⓑ";s:1:"b";s:3:"ⓒ";s:1:"c";s:3:"ⓓ";s:1:"d";s:3:"ⓔ";s:1:"e";s:3:"ⓕ";s:1:"f";s:3:"ⓖ";s:1:"g";s:3:"ⓗ";s:1:"h";s:3:"ⓘ";s:1:"i";s:3:"ⓙ";s:1:"j";s:3:"ⓚ";s:1:"k";s:3:"ⓛ";s:1:"l";s:3:"ⓜ";s:1:"m";s:3:"ⓝ";s:1:"n";s:3:"ⓞ";s:1:"o";s:3:"ⓟ";s:1:"p";s:3:"ⓠ";s:1:"q";s:3:"ⓡ";s:1:"r";s:3:"ⓢ";s:1:"s";s:3:"ⓣ";s:1:"t";s:3:"ⓤ";s:1:"u";s:3:"ⓥ";s:1:"v";s:3:"ⓦ";s:1:"w";s:3:"ⓧ";s:1:"x";s:3:"ⓨ";s:1:"y";s:3:"ⓩ";s:1:"z";s:3:"⓪";s:1:"0";s:3:"⨌";s:12:"∫∫∫∫";s:3:"⩴";s:3:"::=";s:3:"⩵";s:2:"==";s:3:"⩶";s:3:"===";s:3:"⫝̸";s:5:"⫝̸";s:3:"ⱼ";s:1:"j";s:3:"ⱽ";s:1:"V";s:3:"ⵯ";s:3:"ⵡ";s:3:"⺟";s:3:"母";s:3:"⻳";s:3:"龟";s:3:"⼀";s:3:"一";s:3:"⼁";s:3:"丨";s:3:"⼂";s:3:"丶";s:3:"⼃";s:3:"丿";s:3:"⼄";s:3:"乙";s:3:"⼅";s:3:"亅";s:3:"⼆";s:3:"二";s:3:"⼇";s:3:"亠";s:3:"⼈";s:3:"人";s:3:"⼉";s:3:"儿";s:3:"⼊";s:3:"入";s:3:"⼋";s:3:"八";s:3:"⼌";s:3:"冂";s:3:"⼍";s:3:"冖";s:3:"⼎";s:3:"冫";s:3:"⼏";s:3:"几";s:3:"⼐";s:3:"凵";s:3:"⼑";s:3:"刀";s:3:"⼒";s:3:"力";s:3:"⼓";s:3:"勹";s:3:"⼔";s:3:"匕";s:3:"⼕";s:3:"匚";s:3:"⼖";s:3:"匸";s:3:"⼗";s:3:"十";s:3:"⼘";s:3:"卜";s:3:"⼙";s:3:"卩";s:3:"⼚";s:3:"厂";s:3:"⼛";s:3:"厶";s:3:"⼜";s:3:"又";s:3:"⼝";s:3:"口";s:3:"⼞";s:3:"囗";s:3:"⼟";s:3:"土";s:3:"⼠";s:3:"士";s:3:"⼡";s:3:"夂";s:3:"⼢";s:3:"夊";s:3:"⼣";s:3:"夕";s:3:"⼤";s:3:"大";s:3:"⼥";s:3:"女";s:3:"⼦";s:3:"子";s:3:"⼧";s:3:"宀";s:3:"⼨";s:3:"寸";s:3:"⼩";s:3:"小";s:3:"⼪";s:3:"尢";s:3:"⼫";s:3:"尸";s:3:"⼬";s:3:"屮";s:3:"⼭";s:3:"山";s:3:"⼮";s:3:"巛";s:3:"⼯";s:3:"工";s:3:"⼰";s:3:"己";s:3:"⼱";s:3:"巾";s:3:"⼲";s:3:"干";s:3:"⼳";s:3:"幺";s:3:"⼴";s:3:"广";s:3:"⼵";s:3:"廴";s:3:"⼶";s:3:"廾";s:3:"⼷";s:3:"弋";s:3:"⼸";s:3:"弓";s:3:"⼹";s:3:"彐";s:3:"⼺";s:3:"彡";s:3:"⼻";s:3:"彳";s:3:"⼼";s:3:"心";s:3:"⼽";s:3:"戈";s:3:"⼾";s:3:"戶";s:3:"⼿";s:3:"手";s:3:"⽀";s:3:"支";s:3:"⽁";s:3:"攴";s:3:"⽂";s:3:"文";s:3:"⽃";s:3:"斗";s:3:"⽄";s:3:"斤";s:3:"⽅";s:3:"方";s:3:"⽆";s:3:"无";s:3:"⽇";s:3:"日";s:3:"⽈";s:3:"曰";s:3:"⽉";s:3:"月";s:3:"⽊";s:3:"木";s:3:"⽋";s:3:"欠";s:3:"⽌";s:3:"止";s:3:"⽍";s:3:"歹";s:3:"⽎";s:3:"殳";s:3:"⽏";s:3:"毋";s:3:"⽐";s:3:"比";s:3:"⽑";s:3:"毛";s:3:"⽒";s:3:"氏";s:3:"⽓";s:3:"气";s:3:"⽔";s:3:"水";s:3:"⽕";s:3:"火";s:3:"⽖";s:3:"爪";s:3:"⽗";s:3:"父";s:3:"⽘";s:3:"爻";s:3:"⽙";s:3:"爿";s:3:"⽚";s:3:"片";s:3:"⽛";s:3:"牙";s:3:"⽜";s:3:"牛";s:3:"⽝";s:3:"犬";s:3:"⽞";s:3:"玄";s:3:"⽟";s:3:"玉";s:3:"⽠";s:3:"瓜";s:3:"⽡";s:3:"瓦";s:3:"⽢";s:3:"甘";s:3:"⽣";s:3:"生";s:3:"⽤";s:3:"用";s:3:"⽥";s:3:"田";s:3:"⽦";s:3:"疋";s:3:"⽧";s:3:"疒";s:3:"⽨";s:3:"癶";s:3:"⽩";s:3:"白";s:3:"⽪";s:3:"皮";s:3:"⽫";s:3:"皿";s:3:"⽬";s:3:"目";s:3:"⽭";s:3:"矛";s:3:"⽮";s:3:"矢";s:3:"⽯";s:3:"石";s:3:"⽰";s:3:"示";s:3:"⽱";s:3:"禸";s:3:"⽲";s:3:"禾";s:3:"⽳";s:3:"穴";s:3:"⽴";s:3:"立";s:3:"⽵";s:3:"竹";s:3:"⽶";s:3:"米";s:3:"⽷";s:3:"糸";s:3:"⽸";s:3:"缶";s:3:"⽹";s:3:"网";s:3:"⽺";s:3:"羊";s:3:"⽻";s:3:"羽";s:3:"⽼";s:3:"老";s:3:"⽽";s:3:"而";s:3:"⽾";s:3:"耒";s:3:"⽿";s:3:"耳";s:3:"⾀";s:3:"聿";s:3:"⾁";s:3:"肉";s:3:"⾂";s:3:"臣";s:3:"⾃";s:3:"自";s:3:"⾄";s:3:"至";s:3:"⾅";s:3:"臼";s:3:"⾆";s:3:"舌";s:3:"⾇";s:3:"舛";s:3:"⾈";s:3:"舟";s:3:"⾉";s:3:"艮";s:3:"⾊";s:3:"色";s:3:"⾋";s:3:"艸";s:3:"⾌";s:3:"虍";s:3:"⾍";s:3:"虫";s:3:"⾎";s:3:"血";s:3:"⾏";s:3:"行";s:3:"⾐";s:3:"衣";s:3:"⾑";s:3:"襾";s:3:"⾒";s:3:"見";s:3:"⾓";s:3:"角";s:3:"⾔";s:3:"言";s:3:"⾕";s:3:"谷";s:3:"⾖";s:3:"豆";s:3:"⾗";s:3:"豕";s:3:"⾘";s:3:"豸";s:3:"⾙";s:3:"貝";s:3:"⾚";s:3:"赤";s:3:"⾛";s:3:"走";s:3:"⾜";s:3:"足";s:3:"⾝";s:3:"身";s:3:"⾞";s:3:"車";s:3:"⾟";s:3:"辛";s:3:"⾠";s:3:"辰";s:3:"⾡";s:3:"辵";s:3:"⾢";s:3:"邑";s:3:"⾣";s:3:"酉";s:3:"⾤";s:3:"釆";s:3:"⾥";s:3:"里";s:3:"⾦";s:3:"金";s:3:"⾧";s:3:"長";s:3:"⾨";s:3:"門";s:3:"⾩";s:3:"阜";s:3:"⾪";s:3:"隶";s:3:"⾫";s:3:"隹";s:3:"⾬";s:3:"雨";s:3:"⾭";s:3:"靑";s:3:"⾮";s:3:"非";s:3:"⾯";s:3:"面";s:3:"⾰";s:3:"革";s:3:"⾱";s:3:"韋";s:3:"⾲";s:3:"韭";s:3:"⾳";s:3:"音";s:3:"⾴";s:3:"頁";s:3:"⾵";s:3:"風";s:3:"⾶";s:3:"飛";s:3:"⾷";s:3:"食";s:3:"⾸";s:3:"首";s:3:"⾹";s:3:"香";s:3:"⾺";s:3:"馬";s:3:"⾻";s:3:"骨";s:3:"⾼";s:3:"高";s:3:"⾽";s:3:"髟";s:3:"⾾";s:3:"鬥";s:3:"⾿";s:3:"鬯";s:3:"⿀";s:3:"鬲";s:3:"⿁";s:3:"鬼";s:3:"⿂";s:3:"魚";s:3:"⿃";s:3:"鳥";s:3:"⿄";s:3:"鹵";s:3:"⿅";s:3:"鹿";s:3:"⿆";s:3:"麥";s:3:"⿇";s:3:"麻";s:3:"⿈";s:3:"黃";s:3:"⿉";s:3:"黍";s:3:"⿊";s:3:"黑";s:3:"⿋";s:3:"黹";s:3:"⿌";s:3:"黽";s:3:"⿍";s:3:"鼎";s:3:"⿎";s:3:"鼓";s:3:"⿏";s:3:"鼠";s:3:"⿐";s:3:"鼻";s:3:"⿑";s:3:"齊";s:3:"⿒";s:3:"齒";s:3:"⿓";s:3:"龍";s:3:"⿔";s:3:"龜";s:3:"⿕";s:3:"龠";s:3:" ";s:1:" ";s:3:"〶";s:3:"〒";s:3:"〸";s:3:"十";s:3:"〹";s:3:"卄";s:3:"〺";s:3:"卅";s:3:"が";s:6:"が";s:3:"ぎ";s:6:"ぎ";s:3:"ぐ";s:6:"ぐ";s:3:"げ";s:6:"げ";s:3:"ご";s:6:"ご";s:3:"ざ";s:6:"ざ";s:3:"じ";s:6:"じ";s:3:"ず";s:6:"ず";s:3:"ぜ";s:6:"ぜ";s:3:"ぞ";s:6:"ぞ";s:3:"だ";s:6:"だ";s:3:"ぢ";s:6:"ぢ";s:3:"づ";s:6:"づ";s:3:"で";s:6:"で";s:3:"ど";s:6:"ど";s:3:"ば";s:6:"ば";s:3:"ぱ";s:6:"ぱ";s:3:"び";s:6:"び";s:3:"ぴ";s:6:"ぴ";s:3:"ぶ";s:6:"ぶ";s:3:"ぷ";s:6:"ぷ";s:3:"べ";s:6:"べ";s:3:"ぺ";s:6:"ぺ";s:3:"ぼ";s:6:"ぼ";s:3:"ぽ";s:6:"ぽ";s:3:"ゔ";s:6:"ゔ";s:3:"゛";s:4:" ゙";s:3:"゜";s:4:" ゚";s:3:"ゞ";s:6:"ゞ";s:3:"ゟ";s:6:"より";s:3:"ガ";s:6:"ガ";s:3:"ギ";s:6:"ギ";s:3:"グ";s:6:"グ";s:3:"ゲ";s:6:"ゲ";s:3:"ゴ";s:6:"ゴ";s:3:"ザ";s:6:"ザ";s:3:"ジ";s:6:"ジ";s:3:"ズ";s:6:"ズ";s:3:"ゼ";s:6:"ゼ";s:3:"ゾ";s:6:"ゾ";s:3:"ダ";s:6:"ダ";s:3:"ヂ";s:6:"ヂ";s:3:"ヅ";s:6:"ヅ";s:3:"デ";s:6:"デ";s:3:"ド";s:6:"ド";s:3:"バ";s:6:"バ";s:3:"パ";s:6:"パ";s:3:"ビ";s:6:"ビ";s:3:"ピ";s:6:"ピ";s:3:"ブ";s:6:"ブ";s:3:"プ";s:6:"プ";s:3:"ベ";s:6:"ベ";s:3:"ペ";s:6:"ペ";s:3:"ボ";s:6:"ボ";s:3:"ポ";s:6:"ポ";s:3:"ヴ";s:6:"ヴ";s:3:"ヷ";s:6:"ヷ";s:3:"ヸ";s:6:"ヸ";s:3:"ヹ";s:6:"ヹ";s:3:"ヺ";s:6:"ヺ";s:3:"ヾ";s:6:"ヾ";s:3:"ヿ";s:6:"コト";s:3:"ㄱ";s:3:"ᄀ";s:3:"ㄲ";s:3:"ᄁ";s:3:"ㄳ";s:3:"ᆪ";s:3:"ㄴ";s:3:"ᄂ";s:3:"ㄵ";s:3:"ᆬ";s:3:"ㄶ";s:3:"ᆭ";s:3:"ㄷ";s:3:"ᄃ";s:3:"ㄸ";s:3:"ᄄ";s:3:"ㄹ";s:3:"ᄅ";s:3:"ㄺ";s:3:"ᆰ";s:3:"ㄻ";s:3:"ᆱ";s:3:"ㄼ";s:3:"ᆲ";s:3:"ㄽ";s:3:"ᆳ";s:3:"ㄾ";s:3:"ᆴ";s:3:"ㄿ";s:3:"ᆵ";s:3:"ㅀ";s:3:"ᄚ";s:3:"ㅁ";s:3:"ᄆ";s:3:"ㅂ";s:3:"ᄇ";s:3:"ㅃ";s:3:"ᄈ";s:3:"ㅄ";s:3:"ᄡ";s:3:"ㅅ";s:3:"ᄉ";s:3:"ㅆ";s:3:"ᄊ";s:3:"ㅇ";s:3:"ᄋ";s:3:"ㅈ";s:3:"ᄌ";s:3:"ㅉ";s:3:"ᄍ";s:3:"ㅊ";s:3:"ᄎ";s:3:"ㅋ";s:3:"ᄏ";s:3:"ㅌ";s:3:"ᄐ";s:3:"ㅍ";s:3:"ᄑ";s:3:"ㅎ";s:3:"ᄒ";s:3:"ㅏ";s:3:"ᅡ";s:3:"ㅐ";s:3:"ᅢ";s:3:"ㅑ";s:3:"ᅣ";s:3:"ㅒ";s:3:"ᅤ";s:3:"ㅓ";s:3:"ᅥ";s:3:"ㅔ";s:3:"ᅦ";s:3:"ㅕ";s:3:"ᅧ";s:3:"ㅖ";s:3:"ᅨ";s:3:"ㅗ";s:3:"ᅩ";s:3:"ㅘ";s:3:"ᅪ";s:3:"ㅙ";s:3:"ᅫ";s:3:"ㅚ";s:3:"ᅬ";s:3:"ㅛ";s:3:"ᅭ";s:3:"ㅜ";s:3:"ᅮ";s:3:"ㅝ";s:3:"ᅯ";s:3:"ㅞ";s:3:"ᅰ";s:3:"ㅟ";s:3:"ᅱ";s:3:"ㅠ";s:3:"ᅲ";s:3:"ㅡ";s:3:"ᅳ";s:3:"ㅢ";s:3:"ᅴ";s:3:"ㅣ";s:3:"ᅵ";s:3:"ㅤ";s:3:"ᅠ";s:3:"ㅥ";s:3:"ᄔ";s:3:"ㅦ";s:3:"ᄕ";s:3:"ㅧ";s:3:"ᇇ";s:3:"ㅨ";s:3:"ᇈ";s:3:"ㅩ";s:3:"ᇌ";s:3:"ㅪ";s:3:"ᇎ";s:3:"ㅫ";s:3:"ᇓ";s:3:"ㅬ";s:3:"ᇗ";s:3:"ㅭ";s:3:"ᇙ";s:3:"ㅮ";s:3:"ᄜ";s:3:"ㅯ";s:3:"ᇝ";s:3:"ㅰ";s:3:"ᇟ";s:3:"ㅱ";s:3:"ᄝ";s:3:"ㅲ";s:3:"ᄞ";s:3:"ㅳ";s:3:"ᄠ";s:3:"ㅴ";s:3:"ᄢ";s:3:"ㅵ";s:3:"ᄣ";s:3:"ㅶ";s:3:"ᄧ";s:3:"ㅷ";s:3:"ᄩ";s:3:"ㅸ";s:3:"ᄫ";s:3:"ㅹ";s:3:"ᄬ";s:3:"ㅺ";s:3:"ᄭ";s:3:"ㅻ";s:3:"ᄮ";s:3:"ㅼ";s:3:"ᄯ";s:3:"ㅽ";s:3:"ᄲ";s:3:"ㅾ";s:3:"ᄶ";s:3:"ㅿ";s:3:"ᅀ";s:3:"ㆀ";s:3:"ᅇ";s:3:"ㆁ";s:3:"ᅌ";s:3:"ㆂ";s:3:"ᇱ";s:3:"ㆃ";s:3:"ᇲ";s:3:"ㆄ";s:3:"ᅗ";s:3:"ㆅ";s:3:"ᅘ";s:3:"ㆆ";s:3:"ᅙ";s:3:"ㆇ";s:3:"ᆄ";s:3:"ㆈ";s:3:"ᆅ";s:3:"ㆉ";s:3:"ᆈ";s:3:"ㆊ";s:3:"ᆑ";s:3:"ㆋ";s:3:"ᆒ";s:3:"ㆌ";s:3:"ᆔ";s:3:"ㆍ";s:3:"ᆞ";s:3:"ㆎ";s:3:"ᆡ";s:3:"㆒";s:3:"一";s:3:"㆓";s:3:"二";s:3:"㆔";s:3:"三";s:3:"㆕";s:3:"四";s:3:"㆖";s:3:"上";s:3:"㆗";s:3:"中";s:3:"㆘";s:3:"下";s:3:"㆙";s:3:"甲";s:3:"㆚";s:3:"乙";s:3:"㆛";s:3:"丙";s:3:"㆜";s:3:"丁";s:3:"㆝";s:3:"天";s:3:"㆞";s:3:"地";s:3:"㆟";s:3:"人";s:3:"㈀";s:5:"(ᄀ)";s:3:"㈁";s:5:"(ᄂ)";s:3:"㈂";s:5:"(ᄃ)";s:3:"㈃";s:5:"(ᄅ)";s:3:"㈄";s:5:"(ᄆ)";s:3:"㈅";s:5:"(ᄇ)";s:3:"㈆";s:5:"(ᄉ)";s:3:"㈇";s:5:"(ᄋ)";s:3:"㈈";s:5:"(ᄌ)";s:3:"㈉";s:5:"(ᄎ)";s:3:"㈊";s:5:"(ᄏ)";s:3:"㈋";s:5:"(ᄐ)";s:3:"㈌";s:5:"(ᄑ)";s:3:"㈍";s:5:"(ᄒ)";s:3:"㈎";s:8:"(가)";s:3:"㈏";s:8:"(나)";s:3:"㈐";s:8:"(다)";s:3:"㈑";s:8:"(라)";s:3:"㈒";s:8:"(마)";s:3:"㈓";s:8:"(바)";s:3:"㈔";s:8:"(사)";s:3:"㈕";s:8:"(아)";s:3:"㈖";s:8:"(자)";s:3:"㈗";s:8:"(차)";s:3:"㈘";s:8:"(카)";s:3:"㈙";s:8:"(타)";s:3:"㈚";s:8:"(파)";s:3:"㈛";s:8:"(하)";s:3:"㈜";s:8:"(주)";s:3:"㈝";s:17:"(오전)";s:3:"㈞";s:14:"(오후)";s:3:"㈠";s:5:"(一)";s:3:"㈡";s:5:"(二)";s:3:"㈢";s:5:"(三)";s:3:"㈣";s:5:"(四)";s:3:"㈤";s:5:"(五)";s:3:"㈥";s:5:"(六)";s:3:"㈦";s:5:"(七)";s:3:"㈧";s:5:"(八)";s:3:"㈨";s:5:"(九)";s:3:"㈩";s:5:"(十)";s:3:"㈪";s:5:"(月)";s:3:"㈫";s:5:"(火)";s:3:"㈬";s:5:"(水)";s:3:"㈭";s:5:"(木)";s:3:"㈮";s:5:"(金)";s:3:"㈯";s:5:"(土)";s:3:"㈰";s:5:"(日)";s:3:"㈱";s:5:"(株)";s:3:"㈲";s:5:"(有)";s:3:"㈳";s:5:"(社)";s:3:"㈴";s:5:"(名)";s:3:"㈵";s:5:"(特)";s:3:"㈶";s:5:"(財)";s:3:"㈷";s:5:"(祝)";s:3:"㈸";s:5:"(労)";s:3:"㈹";s:5:"(代)";s:3:"㈺";s:5:"(呼)";s:3:"㈻";s:5:"(学)";s:3:"㈼";s:5:"(監)";s:3:"㈽";s:5:"(企)";s:3:"㈾";s:5:"(資)";s:3:"㈿";s:5:"(協)";s:3:"㉀";s:5:"(祭)";s:3:"㉁";s:5:"(休)";s:3:"㉂";s:5:"(自)";s:3:"㉃";s:5:"(至)";s:3:"㉐";s:3:"PTE";s:3:"㉑";s:2:"21";s:3:"㉒";s:2:"22";s:3:"㉓";s:2:"23";s:3:"㉔";s:2:"24";s:3:"㉕";s:2:"25";s:3:"㉖";s:2:"26";s:3:"㉗";s:2:"27";s:3:"㉘";s:2:"28";s:3:"㉙";s:2:"29";s:3:"㉚";s:2:"30";s:3:"㉛";s:2:"31";s:3:"㉜";s:2:"32";s:3:"㉝";s:2:"33";s:3:"㉞";s:2:"34";s:3:"㉟";s:2:"35";s:3:"㉠";s:3:"ᄀ";s:3:"㉡";s:3:"ᄂ";s:3:"㉢";s:3:"ᄃ";s:3:"㉣";s:3:"ᄅ";s:3:"㉤";s:3:"ᄆ";s:3:"㉥";s:3:"ᄇ";s:3:"㉦";s:3:"ᄉ";s:3:"㉧";s:3:"ᄋ";s:3:"㉨";s:3:"ᄌ";s:3:"㉩";s:3:"ᄎ";s:3:"㉪";s:3:"ᄏ";s:3:"㉫";s:3:"ᄐ";s:3:"㉬";s:3:"ᄑ";s:3:"㉭";s:3:"ᄒ";s:3:"㉮";s:6:"가";s:3:"㉯";s:6:"나";s:3:"㉰";s:6:"다";s:3:"㉱";s:6:"라";s:3:"㉲";s:6:"마";s:3:"㉳";s:6:"바";s:3:"㉴";s:6:"사";s:3:"㉵";s:6:"아";s:3:"㉶";s:6:"자";s:3:"㉷";s:6:"차";s:3:"㉸";s:6:"카";s:3:"㉹";s:6:"타";s:3:"㉺";s:6:"파";s:3:"㉻";s:6:"하";s:3:"㉼";s:15:"참고";s:3:"㉽";s:12:"주의";s:3:"㉾";s:6:"우";s:3:"㊀";s:3:"一";s:3:"㊁";s:3:"二";s:3:"㊂";s:3:"三";s:3:"㊃";s:3:"四";s:3:"㊄";s:3:"五";s:3:"㊅";s:3:"六";s:3:"㊆";s:3:"七";s:3:"㊇";s:3:"八";s:3:"㊈";s:3:"九";s:3:"㊉";s:3:"十";s:3:"㊊";s:3:"月";s:3:"㊋";s:3:"火";s:3:"㊌";s:3:"水";s:3:"㊍";s:3:"木";s:3:"㊎";s:3:"金";s:3:"㊏";s:3:"土";s:3:"㊐";s:3:"日";s:3:"㊑";s:3:"株";s:3:"㊒";s:3:"有";s:3:"㊓";s:3:"社";s:3:"㊔";s:3:"名";s:3:"㊕";s:3:"特";s:3:"㊖";s:3:"財";s:3:"㊗";s:3:"祝";s:3:"㊘";s:3:"労";s:3:"㊙";s:3:"秘";s:3:"㊚";s:3:"男";s:3:"㊛";s:3:"女";s:3:"㊜";s:3:"適";s:3:"㊝";s:3:"優";s:3:"㊞";s:3:"印";s:3:"㊟";s:3:"注";s:3:"㊠";s:3:"項";s:3:"㊡";s:3:"休";s:3:"㊢";s:3:"写";s:3:"㊣";s:3:"正";s:3:"㊤";s:3:"上";s:3:"㊥";s:3:"中";s:3:"㊦";s:3:"下";s:3:"㊧";s:3:"左";s:3:"㊨";s:3:"右";s:3:"㊩";s:3:"医";s:3:"㊪";s:3:"宗";s:3:"㊫";s:3:"学";s:3:"㊬";s:3:"監";s:3:"㊭";s:3:"企";s:3:"㊮";s:3:"資";s:3:"㊯";s:3:"協";s:3:"㊰";s:3:"夜";s:3:"㊱";s:2:"36";s:3:"㊲";s:2:"37";s:3:"㊳";s:2:"38";s:3:"㊴";s:2:"39";s:3:"㊵";s:2:"40";s:3:"㊶";s:2:"41";s:3:"㊷";s:2:"42";s:3:"㊸";s:2:"43";s:3:"㊹";s:2:"44";s:3:"㊺";s:2:"45";s:3:"㊻";s:2:"46";s:3:"㊼";s:2:"47";s:3:"㊽";s:2:"48";s:3:"㊾";s:2:"49";s:3:"㊿";s:2:"50";s:3:"㋀";s:4:"1月";s:3:"㋁";s:4:"2月";s:3:"㋂";s:4:"3月";s:3:"㋃";s:4:"4月";s:3:"㋄";s:4:"5月";s:3:"㋅";s:4:"6月";s:3:"㋆";s:4:"7月";s:3:"㋇";s:4:"8月";s:3:"㋈";s:4:"9月";s:3:"㋉";s:5:"10月";s:3:"㋊";s:5:"11月";s:3:"㋋";s:5:"12月";s:3:"㋌";s:2:"Hg";s:3:"㋍";s:3:"erg";s:3:"㋎";s:2:"eV";s:3:"㋏";s:3:"LTD";s:3:"㋐";s:3:"ア";s:3:"㋑";s:3:"イ";s:3:"㋒";s:3:"ウ";s:3:"㋓";s:3:"エ";s:3:"㋔";s:3:"オ";s:3:"㋕";s:3:"カ";s:3:"㋖";s:3:"キ";s:3:"㋗";s:3:"ク";s:3:"㋘";s:3:"ケ";s:3:"㋙";s:3:"コ";s:3:"㋚";s:3:"サ";s:3:"㋛";s:3:"シ";s:3:"㋜";s:3:"ス";s:3:"㋝";s:3:"セ";s:3:"㋞";s:3:"ソ";s:3:"㋟";s:3:"タ";s:3:"㋠";s:3:"チ";s:3:"㋡";s:3:"ツ";s:3:"㋢";s:3:"テ";s:3:"㋣";s:3:"ト";s:3:"㋤";s:3:"ナ";s:3:"㋥";s:3:"ニ";s:3:"㋦";s:3:"ヌ";s:3:"㋧";s:3:"ネ";s:3:"㋨";s:3:"ノ";s:3:"㋩";s:3:"ハ";s:3:"㋪";s:3:"ヒ";s:3:"㋫";s:3:"フ";s:3:"㋬";s:3:"ヘ";s:3:"㋭";s:3:"ホ";s:3:"㋮";s:3:"マ";s:3:"㋯";s:3:"ミ";s:3:"㋰";s:3:"ム";s:3:"㋱";s:3:"メ";s:3:"㋲";s:3:"モ";s:3:"㋳";s:3:"ヤ";s:3:"㋴";s:3:"ユ";s:3:"㋵";s:3:"ヨ";s:3:"㋶";s:3:"ラ";s:3:"㋷";s:3:"リ";s:3:"㋸";s:3:"ル";s:3:"㋹";s:3:"レ";s:3:"㋺";s:3:"ロ";s:3:"㋻";s:3:"ワ";s:3:"㋼";s:3:"ヰ";s:3:"㋽";s:3:"ヱ";s:3:"㋾";s:3:"ヲ";s:3:"㌀";s:15:"アパート";s:3:"㌁";s:12:"アルファ";s:3:"㌂";s:15:"アンペア";s:3:"㌃";s:9:"アール";s:3:"㌄";s:15:"イニング";s:3:"㌅";s:9:"インチ";s:3:"㌆";s:9:"ウォン";s:3:"㌇";s:18:"エスクード";s:3:"㌈";s:12:"エーカー";s:3:"㌉";s:9:"オンス";s:3:"㌊";s:9:"オーム";s:3:"㌋";s:9:"カイリ";s:3:"㌌";s:12:"カラット";s:3:"㌍";s:12:"カロリー";s:3:"㌎";s:12:"ガロン";s:3:"㌏";s:12:"ガンマ";s:3:"㌐";s:12:"ギガ";s:3:"㌑";s:12:"ギニー";s:3:"㌒";s:12:"キュリー";s:3:"㌓";s:18:"ギルダー";s:3:"㌔";s:6:"キロ";s:3:"㌕";s:18:"キログラム";s:3:"㌖";s:18:"キロメートル";s:3:"㌗";s:15:"キロワット";s:3:"㌘";s:12:"グラム";s:3:"㌙";s:18:"グラムトン";s:3:"㌚";s:18:"クルゼイロ";s:3:"㌛";s:12:"クローネ";s:3:"㌜";s:9:"ケース";s:3:"㌝";s:9:"コルナ";s:3:"㌞";s:12:"コーポ";s:3:"㌟";s:12:"サイクル";s:3:"㌠";s:15:"サンチーム";s:3:"㌡";s:15:"シリング";s:3:"㌢";s:9:"センチ";s:3:"㌣";s:9:"セント";s:3:"㌤";s:12:"ダース";s:3:"㌥";s:9:"デシ";s:3:"㌦";s:9:"ドル";s:3:"㌧";s:6:"トン";s:3:"㌨";s:6:"ナノ";s:3:"㌩";s:9:"ノット";s:3:"㌪";s:9:"ハイツ";s:3:"㌫";s:18:"パーセント";s:3:"㌬";s:12:"パーツ";s:3:"㌭";s:15:"バーレル";s:3:"㌮";s:18:"ピアストル";s:3:"㌯";s:12:"ピクル";s:3:"㌰";s:9:"ピコ";s:3:"㌱";s:9:"ビル";s:3:"㌲";s:18:"ファラッド";s:3:"㌳";s:12:"フィート";s:3:"㌴";s:18:"ブッシェル";s:3:"㌵";s:9:"フラン";s:3:"㌶";s:15:"ヘクタール";s:3:"㌷";s:9:"ペソ";s:3:"㌸";s:12:"ペニヒ";s:3:"㌹";s:9:"ヘルツ";s:3:"㌺";s:12:"ペンス";s:3:"㌻";s:15:"ページ";s:3:"㌼";s:12:"ベータ";s:3:"㌽";s:15:"ポイント";s:3:"㌾";s:12:"ボルト";s:3:"㌿";s:6:"ホン";s:3:"㍀";s:15:"ポンド";s:3:"㍁";s:9:"ホール";s:3:"㍂";s:9:"ホーン";s:3:"㍃";s:12:"マイクロ";s:3:"㍄";s:9:"マイル";s:3:"㍅";s:9:"マッハ";s:3:"㍆";s:9:"マルク";s:3:"㍇";s:15:"マンション";s:3:"㍈";s:12:"ミクロン";s:3:"㍉";s:6:"ミリ";s:3:"㍊";s:18:"ミリバール";s:3:"㍋";s:9:"メガ";s:3:"㍌";s:15:"メガトン";s:3:"㍍";s:12:"メートル";s:3:"㍎";s:12:"ヤード";s:3:"㍏";s:9:"ヤール";s:3:"㍐";s:9:"ユアン";s:3:"㍑";s:12:"リットル";s:3:"㍒";s:6:"リラ";s:3:"㍓";s:12:"ルピー";s:3:"㍔";s:15:"ルーブル";s:3:"㍕";s:6:"レム";s:3:"㍖";s:18:"レントゲン";s:3:"㍗";s:9:"ワット";s:3:"㍘";s:4:"0点";s:3:"㍙";s:4:"1点";s:3:"㍚";s:4:"2点";s:3:"㍛";s:4:"3点";s:3:"㍜";s:4:"4点";s:3:"㍝";s:4:"5点";s:3:"㍞";s:4:"6点";s:3:"㍟";s:4:"7点";s:3:"㍠";s:4:"8点";s:3:"㍡";s:4:"9点";s:3:"㍢";s:5:"10点";s:3:"㍣";s:5:"11点";s:3:"㍤";s:5:"12点";s:3:"㍥";s:5:"13点";s:3:"㍦";s:5:"14点";s:3:"㍧";s:5:"15点";s:3:"㍨";s:5:"16点";s:3:"㍩";s:5:"17点";s:3:"㍪";s:5:"18点";s:3:"㍫";s:5:"19点";s:3:"㍬";s:5:"20点";s:3:"㍭";s:5:"21点";s:3:"㍮";s:5:"22点";s:3:"㍯";s:5:"23点";s:3:"㍰";s:5:"24点";s:3:"㍱";s:3:"hPa";s:3:"㍲";s:2:"da";s:3:"㍳";s:2:"AU";s:3:"㍴";s:3:"bar";s:3:"㍵";s:2:"oV";s:3:"㍶";s:2:"pc";s:3:"㍷";s:2:"dm";s:3:"㍸";s:3:"dm2";s:3:"㍹";s:3:"dm3";s:3:"㍺";s:2:"IU";s:3:"㍻";s:6:"平成";s:3:"㍼";s:6:"昭和";s:3:"㍽";s:6:"大正";s:3:"㍾";s:6:"明治";s:3:"㍿";s:12:"株式会社";s:3:"㎀";s:2:"pA";s:3:"㎁";s:2:"nA";s:3:"㎂";s:3:"μA";s:3:"㎃";s:2:"mA";s:3:"㎄";s:2:"kA";s:3:"㎅";s:2:"KB";s:3:"㎆";s:2:"MB";s:3:"㎇";s:2:"GB";s:3:"㎈";s:3:"cal";s:3:"㎉";s:4:"kcal";s:3:"㎊";s:2:"pF";s:3:"㎋";s:2:"nF";s:3:"㎌";s:3:"μF";s:3:"㎍";s:3:"μg";s:3:"㎎";s:2:"mg";s:3:"㎏";s:2:"kg";s:3:"㎐";s:2:"Hz";s:3:"㎑";s:3:"kHz";s:3:"㎒";s:3:"MHz";s:3:"㎓";s:3:"GHz";s:3:"㎔";s:3:"THz";s:3:"㎕";s:3:"μl";s:3:"㎖";s:2:"ml";s:3:"㎗";s:2:"dl";s:3:"㎘";s:2:"kl";s:3:"㎙";s:2:"fm";s:3:"㎚";s:2:"nm";s:3:"㎛";s:3:"μm";s:3:"㎜";s:2:"mm";s:3:"㎝";s:2:"cm";s:3:"㎞";s:2:"km";s:3:"㎟";s:3:"mm2";s:3:"㎠";s:3:"cm2";s:3:"㎡";s:2:"m2";s:3:"㎢";s:3:"km2";s:3:"㎣";s:3:"mm3";s:3:"㎤";s:3:"cm3";s:3:"㎥";s:2:"m3";s:3:"㎦";s:3:"km3";s:3:"㎧";s:5:"m∕s";s:3:"㎨";s:6:"m∕s2";s:3:"㎩";s:2:"Pa";s:3:"㎪";s:3:"kPa";s:3:"㎫";s:3:"MPa";s:3:"㎬";s:3:"GPa";s:3:"㎭";s:3:"rad";s:3:"㎮";s:7:"rad∕s";s:3:"㎯";s:8:"rad∕s2";s:3:"㎰";s:2:"ps";s:3:"㎱";s:2:"ns";s:3:"㎲";s:3:"μs";s:3:"㎳";s:2:"ms";s:3:"㎴";s:2:"pV";s:3:"㎵";s:2:"nV";s:3:"㎶";s:3:"μV";s:3:"㎷";s:2:"mV";s:3:"㎸";s:2:"kV";s:3:"㎹";s:2:"MV";s:3:"㎺";s:2:"pW";s:3:"㎻";s:2:"nW";s:3:"㎼";s:3:"μW";s:3:"㎽";s:2:"mW";s:3:"㎾";s:2:"kW";s:3:"㎿";s:2:"MW";s:3:"㏀";s:3:"kΩ";s:3:"㏁";s:3:"MΩ";s:3:"㏂";s:4:"a.m.";s:3:"㏃";s:2:"Bq";s:3:"㏄";s:2:"cc";s:3:"㏅";s:2:"cd";s:3:"㏆";s:6:"C∕kg";s:3:"㏇";s:3:"Co.";s:3:"㏈";s:2:"dB";s:3:"㏉";s:2:"Gy";s:3:"㏊";s:2:"ha";s:3:"㏋";s:2:"HP";s:3:"㏌";s:2:"in";s:3:"㏍";s:2:"KK";s:3:"㏎";s:2:"KM";s:3:"㏏";s:2:"kt";s:3:"㏐";s:2:"lm";s:3:"㏑";s:2:"ln";s:3:"㏒";s:3:"log";s:3:"㏓";s:2:"lx";s:3:"㏔";s:2:"mb";s:3:"㏕";s:3:"mil";s:3:"㏖";s:3:"mol";s:3:"㏗";s:2:"PH";s:3:"㏘";s:4:"p.m.";s:3:"㏙";s:3:"PPM";s:3:"㏚";s:2:"PR";s:3:"㏛";s:2:"sr";s:3:"㏜";s:2:"Sv";s:3:"㏝";s:2:"Wb";s:3:"㏞";s:5:"V∕m";s:3:"㏟";s:5:"A∕m";s:3:"㏠";s:4:"1日";s:3:"㏡";s:4:"2日";s:3:"㏢";s:4:"3日";s:3:"㏣";s:4:"4日";s:3:"㏤";s:4:"5日";s:3:"㏥";s:4:"6日";s:3:"㏦";s:4:"7日";s:3:"㏧";s:4:"8日";s:3:"㏨";s:4:"9日";s:3:"㏩";s:5:"10日";s:3:"㏪";s:5:"11日";s:3:"㏫";s:5:"12日";s:3:"㏬";s:5:"13日";s:3:"㏭";s:5:"14日";s:3:"㏮";s:5:"15日";s:3:"㏯";s:5:"16日";s:3:"㏰";s:5:"17日";s:3:"㏱";s:5:"18日";s:3:"㏲";s:5:"19日";s:3:"㏳";s:5:"20日";s:3:"㏴";s:5:"21日";s:3:"㏵";s:5:"22日";s:3:"㏶";s:5:"23日";s:3:"㏷";s:5:"24日";s:3:"㏸";s:5:"25日";s:3:"㏹";s:5:"26日";s:3:"㏺";s:5:"27日";s:3:"㏻";s:5:"28日";s:3:"㏼";s:5:"29日";s:3:"㏽";s:5:"30日";s:3:"㏾";s:5:"31日";s:3:"㏿";s:3:"gal";s:3:"ꝰ";s:3:"ꝯ";s:3:"豈";s:3:"豈";s:3:"更";s:3:"更";s:3:"車";s:3:"車";s:3:"賈";s:3:"賈";s:3:"滑";s:3:"滑";s:3:"串";s:3:"串";s:3:"句";s:3:"句";s:3:"龜";s:3:"龜";s:3:"龜";s:3:"龜";s:3:"契";s:3:"契";s:3:"金";s:3:"金";s:3:"喇";s:3:"喇";s:3:"奈";s:3:"奈";s:3:"懶";s:3:"懶";s:3:"癩";s:3:"癩";s:3:"羅";s:3:"羅";s:3:"蘿";s:3:"蘿";s:3:"螺";s:3:"螺";s:3:"裸";s:3:"裸";s:3:"邏";s:3:"邏";s:3:"樂";s:3:"樂";s:3:"洛";s:3:"洛";s:3:"烙";s:3:"烙";s:3:"珞";s:3:"珞";s:3:"落";s:3:"落";s:3:"酪";s:3:"酪";s:3:"駱";s:3:"駱";s:3:"亂";s:3:"亂";s:3:"卵";s:3:"卵";s:3:"欄";s:3:"欄";s:3:"爛";s:3:"爛";s:3:"蘭";s:3:"蘭";s:3:"鸞";s:3:"鸞";s:3:"嵐";s:3:"嵐";s:3:"濫";s:3:"濫";s:3:"藍";s:3:"藍";s:3:"襤";s:3:"襤";s:3:"拉";s:3:"拉";s:3:"臘";s:3:"臘";s:3:"蠟";s:3:"蠟";s:3:"廊";s:3:"廊";s:3:"朗";s:3:"朗";s:3:"浪";s:3:"浪";s:3:"狼";s:3:"狼";s:3:"郎";s:3:"郎";s:3:"來";s:3:"來";s:3:"冷";s:3:"冷";s:3:"勞";s:3:"勞";s:3:"擄";s:3:"擄";s:3:"櫓";s:3:"櫓";s:3:"爐";s:3:"爐";s:3:"盧";s:3:"盧";s:3:"老";s:3:"老";s:3:"蘆";s:3:"蘆";s:3:"虜";s:3:"虜";s:3:"路";s:3:"路";s:3:"露";s:3:"露";s:3:"魯";s:3:"魯";s:3:"鷺";s:3:"鷺";s:3:"碌";s:3:"碌";s:3:"祿";s:3:"祿";s:3:"綠";s:3:"綠";s:3:"菉";s:3:"菉";s:3:"錄";s:3:"錄";s:3:"鹿";s:3:"鹿";s:3:"論";s:3:"論";s:3:"壟";s:3:"壟";s:3:"弄";s:3:"弄";s:3:"籠";s:3:"籠";s:3:"聾";s:3:"聾";s:3:"牢";s:3:"牢";s:3:"磊";s:3:"磊";s:3:"賂";s:3:"賂";s:3:"雷";s:3:"雷";s:3:"壘";s:3:"壘";s:3:"屢";s:3:"屢";s:3:"樓";s:3:"樓";s:3:"淚";s:3:"淚";s:3:"漏";s:3:"漏";s:3:"累";s:3:"累";s:3:"縷";s:3:"縷";s:3:"陋";s:3:"陋";s:3:"勒";s:3:"勒";s:3:"肋";s:3:"肋";s:3:"凜";s:3:"凜";s:3:"凌";s:3:"凌";s:3:"稜";s:3:"稜";s:3:"綾";s:3:"綾";s:3:"菱";s:3:"菱";s:3:"陵";s:3:"陵";s:3:"讀";s:3:"讀";s:3:"拏";s:3:"拏";s:3:"樂";s:3:"樂";s:3:"諾";s:3:"諾";s:3:"丹";s:3:"丹";s:3:"寧";s:3:"寧";s:3:"怒";s:3:"怒";s:3:"率";s:3:"率";s:3:"異";s:3:"異";s:3:"北";s:3:"北";s:3:"磻";s:3:"磻";s:3:"便";s:3:"便";s:3:"復";s:3:"復";s:3:"不";s:3:"不";s:3:"泌";s:3:"泌";s:3:"數";s:3:"數";s:3:"索";s:3:"索";s:3:"參";s:3:"參";s:3:"塞";s:3:"塞";s:3:"省";s:3:"省";s:3:"葉";s:3:"葉";s:3:"說";s:3:"說";s:3:"殺";s:3:"殺";s:3:"辰";s:3:"辰";s:3:"沈";s:3:"沈";s:3:"拾";s:3:"拾";s:3:"若";s:3:"若";s:3:"掠";s:3:"掠";s:3:"略";s:3:"略";s:3:"亮";s:3:"亮";s:3:"兩";s:3:"兩";s:3:"凉";s:3:"凉";s:3:"梁";s:3:"梁";s:3:"糧";s:3:"糧";s:3:"良";s:3:"良";s:3:"諒";s:3:"諒";s:3:"量";s:3:"量";s:3:"勵";s:3:"勵";s:3:"呂";s:3:"呂";s:3:"女";s:3:"女";s:3:"廬";s:3:"廬";s:3:"旅";s:3:"旅";s:3:"濾";s:3:"濾";s:3:"礪";s:3:"礪";s:3:"閭";s:3:"閭";s:3:"驪";s:3:"驪";s:3:"麗";s:3:"麗";s:3:"黎";s:3:"黎";s:3:"力";s:3:"力";s:3:"曆";s:3:"曆";s:3:"歷";s:3:"歷";s:3:"轢";s:3:"轢";s:3:"年";s:3:"年";s:3:"憐";s:3:"憐";s:3:"戀";s:3:"戀";s:3:"撚";s:3:"撚";s:3:"漣";s:3:"漣";s:3:"煉";s:3:"煉";s:3:"璉";s:3:"璉";s:3:"秊";s:3:"秊";s:3:"練";s:3:"練";s:3:"聯";s:3:"聯";s:3:"輦";s:3:"輦";s:3:"蓮";s:3:"蓮";s:3:"連";s:3:"連";s:3:"鍊";s:3:"鍊";s:3:"列";s:3:"列";s:3:"劣";s:3:"劣";s:3:"咽";s:3:"咽";s:3:"烈";s:3:"烈";s:3:"裂";s:3:"裂";s:3:"說";s:3:"說";s:3:"廉";s:3:"廉";s:3:"念";s:3:"念";s:3:"捻";s:3:"捻";s:3:"殮";s:3:"殮";s:3:"簾";s:3:"簾";s:3:"獵";s:3:"獵";s:3:"令";s:3:"令";s:3:"囹";s:3:"囹";s:3:"寧";s:3:"寧";s:3:"嶺";s:3:"嶺";s:3:"怜";s:3:"怜";s:3:"玲";s:3:"玲";s:3:"瑩";s:3:"瑩";s:3:"羚";s:3:"羚";s:3:"聆";s:3:"聆";s:3:"鈴";s:3:"鈴";s:3:"零";s:3:"零";s:3:"靈";s:3:"靈";s:3:"領";s:3:"領";s:3:"例";s:3:"例";s:3:"禮";s:3:"禮";s:3:"醴";s:3:"醴";s:3:"隸";s:3:"隸";s:3:"惡";s:3:"惡";s:3:"了";s:3:"了";s:3:"僚";s:3:"僚";s:3:"寮";s:3:"寮";s:3:"尿";s:3:"尿";s:3:"料";s:3:"料";s:3:"樂";s:3:"樂";s:3:"燎";s:3:"燎";s:3:"療";s:3:"療";s:3:"蓼";s:3:"蓼";s:3:"遼";s:3:"遼";s:3:"龍";s:3:"龍";s:3:"暈";s:3:"暈";s:3:"阮";s:3:"阮";s:3:"劉";s:3:"劉";s:3:"杻";s:3:"杻";s:3:"柳";s:3:"柳";s:3:"流";s:3:"流";s:3:"溜";s:3:"溜";s:3:"琉";s:3:"琉";s:3:"留";s:3:"留";s:3:"硫";s:3:"硫";s:3:"紐";s:3:"紐";s:3:"類";s:3:"類";s:3:"六";s:3:"六";s:3:"戮";s:3:"戮";s:3:"陸";s:3:"陸";s:3:"倫";s:3:"倫";s:3:"崙";s:3:"崙";s:3:"淪";s:3:"淪";s:3:"輪";s:3:"輪";s:3:"律";s:3:"律";s:3:"慄";s:3:"慄";s:3:"栗";s:3:"栗";s:3:"率";s:3:"率";s:3:"隆";s:3:"隆";s:3:"利";s:3:"利";s:3:"吏";s:3:"吏";s:3:"履";s:3:"履";s:3:"易";s:3:"易";s:3:"李";s:3:"李";s:3:"梨";s:3:"梨";s:3:"泥";s:3:"泥";s:3:"理";s:3:"理";s:3:"痢";s:3:"痢";s:3:"罹";s:3:"罹";s:3:"裏";s:3:"裏";s:3:"裡";s:3:"裡";s:3:"里";s:3:"里";s:3:"離";s:3:"離";s:3:"匿";s:3:"匿";s:3:"溺";s:3:"溺";s:3:"吝";s:3:"吝";s:3:"燐";s:3:"燐";s:3:"璘";s:3:"璘";s:3:"藺";s:3:"藺";s:3:"隣";s:3:"隣";s:3:"鱗";s:3:"鱗";s:3:"麟";s:3:"麟";s:3:"林";s:3:"林";s:3:"淋";s:3:"淋";s:3:"臨";s:3:"臨";s:3:"立";s:3:"立";s:3:"笠";s:3:"笠";s:3:"粒";s:3:"粒";s:3:"狀";s:3:"狀";s:3:"炙";s:3:"炙";s:3:"識";s:3:"識";s:3:"什";s:3:"什";s:3:"茶";s:3:"茶";s:3:"刺";s:3:"刺";s:3:"切";s:3:"切";s:3:"度";s:3:"度";s:3:"拓";s:3:"拓";s:3:"糖";s:3:"糖";s:3:"宅";s:3:"宅";s:3:"洞";s:3:"洞";s:3:"暴";s:3:"暴";s:3:"輻";s:3:"輻";s:3:"行";s:3:"行";s:3:"降";s:3:"降";s:3:"見";s:3:"見";s:3:"廓";s:3:"廓";s:3:"兀";s:3:"兀";s:3:"嗀";s:3:"嗀";s:3:"塚";s:3:"塚";s:3:"晴";s:3:"晴";s:3:"凞";s:3:"凞";s:3:"猪";s:3:"猪";s:3:"益";s:3:"益";s:3:"礼";s:3:"礼";s:3:"神";s:3:"神";s:3:"祥";s:3:"祥";s:3:"福";s:3:"福";s:3:"靖";s:3:"靖";s:3:"精";s:3:"精";s:3:"羽";s:3:"羽";s:3:"蘒";s:3:"蘒";s:3:"諸";s:3:"諸";s:3:"逸";s:3:"逸";s:3:"都";s:3:"都";s:3:"飯";s:3:"飯";s:3:"飼";s:3:"飼";s:3:"館";s:3:"館";s:3:"鶴";s:3:"鶴";s:3:"侮";s:3:"侮";s:3:"僧";s:3:"僧";s:3:"免";s:3:"免";s:3:"勉";s:3:"勉";s:3:"勤";s:3:"勤";s:3:"卑";s:3:"卑";s:3:"喝";s:3:"喝";s:3:"嘆";s:3:"嘆";s:3:"器";s:3:"器";s:3:"塀";s:3:"塀";s:3:"墨";s:3:"墨";s:3:"層";s:3:"層";s:3:"屮";s:3:"屮";s:3:"悔";s:3:"悔";s:3:"慨";s:3:"慨";s:3:"憎";s:3:"憎";s:3:"懲";s:3:"懲";s:3:"敏";s:3:"敏";s:3:"既";s:3:"既";s:3:"暑";s:3:"暑";s:3:"梅";s:3:"梅";s:3:"海";s:3:"海";s:3:"渚";s:3:"渚";s:3:"漢";s:3:"漢";s:3:"煮";s:3:"煮";s:3:"爫";s:3:"爫";s:3:"琢";s:3:"琢";s:3:"碑";s:3:"碑";s:3:"社";s:3:"社";s:3:"祉";s:3:"祉";s:3:"祈";s:3:"祈";s:3:"祐";s:3:"祐";s:3:"祖";s:3:"祖";s:3:"祝";s:3:"祝";s:3:"禍";s:3:"禍";s:3:"禎";s:3:"禎";s:3:"穀";s:3:"穀";s:3:"突";s:3:"突";s:3:"節";s:3:"節";s:3:"練";s:3:"練";s:3:"縉";s:3:"縉";s:3:"繁";s:3:"繁";s:3:"署";s:3:"署";s:3:"者";s:3:"者";s:3:"臭";s:3:"臭";s:3:"艹";s:3:"艹";s:3:"艹";s:3:"艹";s:3:"著";s:3:"著";s:3:"褐";s:3:"褐";s:3:"視";s:3:"視";s:3:"謁";s:3:"謁";s:3:"謹";s:3:"謹";s:3:"賓";s:3:"賓";s:3:"贈";s:3:"贈";s:3:"辶";s:3:"辶";s:3:"逸";s:3:"逸";s:3:"難";s:3:"難";s:3:"響";s:3:"響";s:3:"頻";s:3:"頻";s:3:"並";s:3:"並";s:3:"况";s:3:"况";s:3:"全";s:3:"全";s:3:"侀";s:3:"侀";s:3:"充";s:3:"充";s:3:"冀";s:3:"冀";s:3:"勇";s:3:"勇";s:3:"勺";s:3:"勺";s:3:"喝";s:3:"喝";s:3:"啕";s:3:"啕";s:3:"喙";s:3:"喙";s:3:"嗢";s:3:"嗢";s:3:"塚";s:3:"塚";s:3:"墳";s:3:"墳";s:3:"奄";s:3:"奄";s:3:"奔";s:3:"奔";s:3:"婢";s:3:"婢";s:3:"嬨";s:3:"嬨";s:3:"廒";s:3:"廒";s:3:"廙";s:3:"廙";s:3:"彩";s:3:"彩";s:3:"徭";s:3:"徭";s:3:"惘";s:3:"惘";s:3:"慎";s:3:"慎";s:3:"愈";s:3:"愈";s:3:"憎";s:3:"憎";s:3:"慠";s:3:"慠";s:3:"懲";s:3:"懲";s:3:"戴";s:3:"戴";s:3:"揄";s:3:"揄";s:3:"搜";s:3:"搜";s:3:"摒";s:3:"摒";s:3:"敖";s:3:"敖";s:3:"晴";s:3:"晴";s:3:"朗";s:3:"朗";s:3:"望";s:3:"望";s:3:"杖";s:3:"杖";s:3:"歹";s:3:"歹";s:3:"殺";s:3:"殺";s:3:"流";s:3:"流";s:3:"滛";s:3:"滛";s:3:"滋";s:3:"滋";s:3:"漢";s:3:"漢";s:3:"瀞";s:3:"瀞";s:3:"煮";s:3:"煮";s:3:"瞧";s:3:"瞧";s:3:"爵";s:3:"爵";s:3:"犯";s:3:"犯";s:3:"猪";s:3:"猪";s:3:"瑱";s:3:"瑱";s:3:"甆";s:3:"甆";s:3:"画";s:3:"画";s:3:"瘝";s:3:"瘝";s:3:"瘟";s:3:"瘟";s:3:"益";s:3:"益";s:3:"盛";s:3:"盛";s:3:"直";s:3:"直";s:3:"睊";s:3:"睊";s:3:"着";s:3:"着";s:3:"磌";s:3:"磌";s:3:"窱";s:3:"窱";s:3:"節";s:3:"節";s:3:"类";s:3:"类";s:3:"絛";s:3:"絛";s:3:"練";s:3:"練";s:3:"缾";s:3:"缾";s:3:"者";s:3:"者";s:3:"荒";s:3:"荒";s:3:"華";s:3:"華";s:3:"蝹";s:3:"蝹";s:3:"襁";s:3:"襁";s:3:"覆";s:3:"覆";s:3:"視";s:3:"視";s:3:"調";s:3:"調";s:3:"諸";s:3:"諸";s:3:"請";s:3:"請";s:3:"謁";s:3:"謁";s:3:"諾";s:3:"諾";s:3:"諭";s:3:"諭";s:3:"謹";s:3:"謹";s:3:"變";s:3:"變";s:3:"贈";s:3:"贈";s:3:"輸";s:3:"輸";s:3:"遲";s:3:"遲";s:3:"醙";s:3:"醙";s:3:"鉶";s:3:"鉶";s:3:"陼";s:3:"陼";s:3:"難";s:3:"難";s:3:"靖";s:3:"靖";s:3:"韛";s:3:"韛";s:3:"響";s:3:"響";s:3:"頋";s:3:"頋";s:3:"頻";s:3:"頻";s:3:"鬒";s:3:"鬒";s:3:"龜";s:3:"龜";s:3:"𢡊";s:4:"𢡊";s:3:"𢡄";s:4:"𢡄";s:3:"𣏕";s:4:"𣏕";s:3:"㮝";s:3:"㮝";s:3:"䀘";s:3:"䀘";s:3:"䀹";s:3:"䀹";s:3:"𥉉";s:4:"𥉉";s:3:"𥳐";s:4:"𥳐";s:3:"𧻓";s:4:"𧻓";s:3:"齃";s:3:"齃";s:3:"龎";s:3:"龎";s:3:"ff";s:2:"ff";s:3:"fi";s:2:"fi";s:3:"fl";s:2:"fl";s:3:"ffi";s:3:"ffi";s:3:"ffl";s:3:"ffl";s:3:"ſt";s:2:"st";s:3:"st";s:2:"st";s:3:"ﬓ";s:4:"մն";s:3:"ﬔ";s:4:"մե";s:3:"ﬕ";s:4:"մի";s:3:"ﬖ";s:4:"վն";s:3:"ﬗ";s:4:"մխ";s:3:"יִ";s:4:"יִ";s:3:"ײַ";s:4:"ײַ";s:3:"ﬠ";s:2:"ע";s:3:"ﬡ";s:2:"א";s:3:"ﬢ";s:2:"ד";s:3:"ﬣ";s:2:"ה";s:3:"ﬤ";s:2:"כ";s:3:"ﬥ";s:2:"ל";s:3:"ﬦ";s:2:"ם";s:3:"ﬧ";s:2:"ר";s:3:"ﬨ";s:2:"ת";s:3:"﬩";s:1:"+";s:3:"שׁ";s:4:"שׁ";s:3:"שׂ";s:4:"שׂ";s:3:"שּׁ";s:6:"שּׁ";s:3:"שּׂ";s:6:"שּׂ";s:3:"אַ";s:4:"אַ";s:3:"אָ";s:4:"אָ";s:3:"אּ";s:4:"אּ";s:3:"בּ";s:4:"בּ";s:3:"גּ";s:4:"גּ";s:3:"דּ";s:4:"דּ";s:3:"הּ";s:4:"הּ";s:3:"וּ";s:4:"וּ";s:3:"זּ";s:4:"זּ";s:3:"טּ";s:4:"טּ";s:3:"יּ";s:4:"יּ";s:3:"ךּ";s:4:"ךּ";s:3:"כּ";s:4:"כּ";s:3:"לּ";s:4:"לּ";s:3:"מּ";s:4:"מּ";s:3:"נּ";s:4:"נּ";s:3:"סּ";s:4:"סּ";s:3:"ףּ";s:4:"ףּ";s:3:"פּ";s:4:"פּ";s:3:"צּ";s:4:"צּ";s:3:"קּ";s:4:"קּ";s:3:"רּ";s:4:"רּ";s:3:"שּ";s:4:"שּ";s:3:"תּ";s:4:"תּ";s:3:"וֹ";s:4:"וֹ";s:3:"בֿ";s:4:"בֿ";s:3:"כֿ";s:4:"כֿ";s:3:"פֿ";s:4:"פֿ";s:3:"ﭏ";s:4:"אל";s:3:"ﭐ";s:2:"ٱ";s:3:"ﭑ";s:2:"ٱ";s:3:"ﭒ";s:2:"ٻ";s:3:"ﭓ";s:2:"ٻ";s:3:"ﭔ";s:2:"ٻ";s:3:"ﭕ";s:2:"ٻ";s:3:"ﭖ";s:2:"پ";s:3:"ﭗ";s:2:"پ";s:3:"ﭘ";s:2:"پ";s:3:"ﭙ";s:2:"پ";s:3:"ﭚ";s:2:"ڀ";s:3:"ﭛ";s:2:"ڀ";s:3:"ﭜ";s:2:"ڀ";s:3:"ﭝ";s:2:"ڀ";s:3:"ﭞ";s:2:"ٺ";s:3:"ﭟ";s:2:"ٺ";s:3:"ﭠ";s:2:"ٺ";s:3:"ﭡ";s:2:"ٺ";s:3:"ﭢ";s:2:"ٿ";s:3:"ﭣ";s:2:"ٿ";s:3:"ﭤ";s:2:"ٿ";s:3:"ﭥ";s:2:"ٿ";s:3:"ﭦ";s:2:"ٹ";s:3:"ﭧ";s:2:"ٹ";s:3:"ﭨ";s:2:"ٹ";s:3:"ﭩ";s:2:"ٹ";s:3:"ﭪ";s:2:"ڤ";s:3:"ﭫ";s:2:"ڤ";s:3:"ﭬ";s:2:"ڤ";s:3:"ﭭ";s:2:"ڤ";s:3:"ﭮ";s:2:"ڦ";s:3:"ﭯ";s:2:"ڦ";s:3:"ﭰ";s:2:"ڦ";s:3:"ﭱ";s:2:"ڦ";s:3:"ﭲ";s:2:"ڄ";s:3:"ﭳ";s:2:"ڄ";s:3:"ﭴ";s:2:"ڄ";s:3:"ﭵ";s:2:"ڄ";s:3:"ﭶ";s:2:"ڃ";s:3:"ﭷ";s:2:"ڃ";s:3:"ﭸ";s:2:"ڃ";s:3:"ﭹ";s:2:"ڃ";s:3:"ﭺ";s:2:"چ";s:3:"ﭻ";s:2:"چ";s:3:"ﭼ";s:2:"چ";s:3:"ﭽ";s:2:"چ";s:3:"ﭾ";s:2:"ڇ";s:3:"ﭿ";s:2:"ڇ";s:3:"ﮀ";s:2:"ڇ";s:3:"ﮁ";s:2:"ڇ";s:3:"ﮂ";s:2:"ڍ";s:3:"ﮃ";s:2:"ڍ";s:3:"ﮄ";s:2:"ڌ";s:3:"ﮅ";s:2:"ڌ";s:3:"ﮆ";s:2:"ڎ";s:3:"ﮇ";s:2:"ڎ";s:3:"ﮈ";s:2:"ڈ";s:3:"ﮉ";s:2:"ڈ";s:3:"ﮊ";s:2:"ژ";s:3:"ﮋ";s:2:"ژ";s:3:"ﮌ";s:2:"ڑ";s:3:"ﮍ";s:2:"ڑ";s:3:"ﮎ";s:2:"ک";s:3:"ﮏ";s:2:"ک";s:3:"ﮐ";s:2:"ک";s:3:"ﮑ";s:2:"ک";s:3:"ﮒ";s:2:"گ";s:3:"ﮓ";s:2:"گ";s:3:"ﮔ";s:2:"گ";s:3:"ﮕ";s:2:"گ";s:3:"ﮖ";s:2:"ڳ";s:3:"ﮗ";s:2:"ڳ";s:3:"ﮘ";s:2:"ڳ";s:3:"ﮙ";s:2:"ڳ";s:3:"ﮚ";s:2:"ڱ";s:3:"ﮛ";s:2:"ڱ";s:3:"ﮜ";s:2:"ڱ";s:3:"ﮝ";s:2:"ڱ";s:3:"ﮞ";s:2:"ں";s:3:"ﮟ";s:2:"ں";s:3:"ﮠ";s:2:"ڻ";s:3:"ﮡ";s:2:"ڻ";s:3:"ﮢ";s:2:"ڻ";s:3:"ﮣ";s:2:"ڻ";s:3:"ﮤ";s:4:"ۀ";s:3:"ﮥ";s:4:"ۀ";s:3:"ﮦ";s:2:"ہ";s:3:"ﮧ";s:2:"ہ";s:3:"ﮨ";s:2:"ہ";s:3:"ﮩ";s:2:"ہ";s:3:"ﮪ";s:2:"ھ";s:3:"ﮫ";s:2:"ھ";s:3:"ﮬ";s:2:"ھ";s:3:"ﮭ";s:2:"ھ";s:3:"ﮮ";s:2:"ے";s:3:"ﮯ";s:2:"ے";s:3:"ﮰ";s:4:"ۓ";s:3:"ﮱ";s:4:"ۓ";s:3:"ﯓ";s:2:"ڭ";s:3:"ﯔ";s:2:"ڭ";s:3:"ﯕ";s:2:"ڭ";s:3:"ﯖ";s:2:"ڭ";s:3:"ﯗ";s:2:"ۇ";s:3:"ﯘ";s:2:"ۇ";s:3:"ﯙ";s:2:"ۆ";s:3:"ﯚ";s:2:"ۆ";s:3:"ﯛ";s:2:"ۈ";s:3:"ﯜ";s:2:"ۈ";s:3:"ﯝ";s:4:"ۇٴ";s:3:"ﯞ";s:2:"ۋ";s:3:"ﯟ";s:2:"ۋ";s:3:"ﯠ";s:2:"ۅ";s:3:"ﯡ";s:2:"ۅ";s:3:"ﯢ";s:2:"ۉ";s:3:"ﯣ";s:2:"ۉ";s:3:"ﯤ";s:2:"ې";s:3:"ﯥ";s:2:"ې";s:3:"ﯦ";s:2:"ې";s:3:"ﯧ";s:2:"ې";s:3:"ﯨ";s:2:"ى";s:3:"ﯩ";s:2:"ى";s:3:"ﯪ";s:6:"ئا";s:3:"ﯫ";s:6:"ئا";s:3:"ﯬ";s:6:"ئە";s:3:"ﯭ";s:6:"ئە";s:3:"ﯮ";s:6:"ئو";s:3:"ﯯ";s:6:"ئو";s:3:"ﯰ";s:6:"ئۇ";s:3:"ﯱ";s:6:"ئۇ";s:3:"ﯲ";s:6:"ئۆ";s:3:"ﯳ";s:6:"ئۆ";s:3:"ﯴ";s:6:"ئۈ";s:3:"ﯵ";s:6:"ئۈ";s:3:"ﯶ";s:6:"ئې";s:3:"ﯷ";s:6:"ئې";s:3:"ﯸ";s:6:"ئې";s:3:"ﯹ";s:6:"ئى";s:3:"ﯺ";s:6:"ئى";s:3:"ﯻ";s:6:"ئى";s:3:"ﯼ";s:2:"ی";s:3:"ﯽ";s:2:"ی";s:3:"ﯾ";s:2:"ی";s:3:"ﯿ";s:2:"ی";s:3:"ﰀ";s:6:"ئج";s:3:"ﰁ";s:6:"ئح";s:3:"ﰂ";s:6:"ئم";s:3:"ﰃ";s:6:"ئى";s:3:"ﰄ";s:6:"ئي";s:3:"ﰅ";s:4:"بج";s:3:"ﰆ";s:4:"بح";s:3:"ﰇ";s:4:"بخ";s:3:"ﰈ";s:4:"بم";s:3:"ﰉ";s:4:"بى";s:3:"ﰊ";s:4:"بي";s:3:"ﰋ";s:4:"تج";s:3:"ﰌ";s:4:"تح";s:3:"ﰍ";s:4:"تخ";s:3:"ﰎ";s:4:"تم";s:3:"ﰏ";s:4:"تى";s:3:"ﰐ";s:4:"تي";s:3:"ﰑ";s:4:"ثج";s:3:"ﰒ";s:4:"ثم";s:3:"ﰓ";s:4:"ثى";s:3:"ﰔ";s:4:"ثي";s:3:"ﰕ";s:4:"جح";s:3:"ﰖ";s:4:"جم";s:3:"ﰗ";s:4:"حج";s:3:"ﰘ";s:4:"حم";s:3:"ﰙ";s:4:"خج";s:3:"ﰚ";s:4:"خح";s:3:"ﰛ";s:4:"خم";s:3:"ﰜ";s:4:"سج";s:3:"ﰝ";s:4:"سح";s:3:"ﰞ";s:4:"سخ";s:3:"ﰟ";s:4:"سم";s:3:"ﰠ";s:4:"صح";s:3:"ﰡ";s:4:"صم";s:3:"ﰢ";s:4:"ضج";s:3:"ﰣ";s:4:"ضح";s:3:"ﰤ";s:4:"ضخ";s:3:"ﰥ";s:4:"ضم";s:3:"ﰦ";s:4:"طح";s:3:"ﰧ";s:4:"طم";s:3:"ﰨ";s:4:"ظم";s:3:"ﰩ";s:4:"عج";s:3:"ﰪ";s:4:"عم";s:3:"ﰫ";s:4:"غج";s:3:"ﰬ";s:4:"غم";s:3:"ﰭ";s:4:"فج";s:3:"ﰮ";s:4:"فح";s:3:"ﰯ";s:4:"فخ";s:3:"ﰰ";s:4:"فم";s:3:"ﰱ";s:4:"فى";s:3:"ﰲ";s:4:"في";s:3:"ﰳ";s:4:"قح";s:3:"ﰴ";s:4:"قم";s:3:"ﰵ";s:4:"قى";s:3:"ﰶ";s:4:"قي";s:3:"ﰷ";s:4:"كا";s:3:"ﰸ";s:4:"كج";s:3:"ﰹ";s:4:"كح";s:3:"ﰺ";s:4:"كخ";s:3:"ﰻ";s:4:"كل";s:3:"ﰼ";s:4:"كم";s:3:"ﰽ";s:4:"كى";s:3:"ﰾ";s:4:"كي";s:3:"ﰿ";s:4:"لج";s:3:"ﱀ";s:4:"لح";s:3:"ﱁ";s:4:"لخ";s:3:"ﱂ";s:4:"لم";s:3:"ﱃ";s:4:"لى";s:3:"ﱄ";s:4:"لي";s:3:"ﱅ";s:4:"مج";s:3:"ﱆ";s:4:"مح";s:3:"ﱇ";s:4:"مخ";s:3:"ﱈ";s:4:"مم";s:3:"ﱉ";s:4:"مى";s:3:"ﱊ";s:4:"مي";s:3:"ﱋ";s:4:"نج";s:3:"ﱌ";s:4:"نح";s:3:"ﱍ";s:4:"نخ";s:3:"ﱎ";s:4:"نم";s:3:"ﱏ";s:4:"نى";s:3:"ﱐ";s:4:"ني";s:3:"ﱑ";s:4:"هج";s:3:"ﱒ";s:4:"هم";s:3:"ﱓ";s:4:"هى";s:3:"ﱔ";s:4:"هي";s:3:"ﱕ";s:4:"يج";s:3:"ﱖ";s:4:"يح";s:3:"ﱗ";s:4:"يخ";s:3:"ﱘ";s:4:"يم";s:3:"ﱙ";s:4:"يى";s:3:"ﱚ";s:4:"يي";s:3:"ﱛ";s:4:"ذٰ";s:3:"ﱜ";s:4:"رٰ";s:3:"ﱝ";s:4:"ىٰ";s:3:"ﱞ";s:5:" ٌّ";s:3:"ﱟ";s:5:" ٍّ";s:3:"ﱠ";s:5:" َّ";s:3:"ﱡ";s:5:" ُّ";s:3:"ﱢ";s:5:" ِّ";s:3:"ﱣ";s:5:" ّٰ";s:3:"ﱤ";s:6:"ئر";s:3:"ﱥ";s:6:"ئز";s:3:"ﱦ";s:6:"ئم";s:3:"ﱧ";s:6:"ئن";s:3:"ﱨ";s:6:"ئى";s:3:"ﱩ";s:6:"ئي";s:3:"ﱪ";s:4:"بر";s:3:"ﱫ";s:4:"بز";s:3:"ﱬ";s:4:"بم";s:3:"ﱭ";s:4:"بن";s:3:"ﱮ";s:4:"بى";s:3:"ﱯ";s:4:"بي";s:3:"ﱰ";s:4:"تر";s:3:"ﱱ";s:4:"تز";s:3:"ﱲ";s:4:"تم";s:3:"ﱳ";s:4:"تن";s:3:"ﱴ";s:4:"تى";s:3:"ﱵ";s:4:"تي";s:3:"ﱶ";s:4:"ثر";s:3:"ﱷ";s:4:"ثز";s:3:"ﱸ";s:4:"ثم";s:3:"ﱹ";s:4:"ثن";s:3:"ﱺ";s:4:"ثى";s:3:"ﱻ";s:4:"ثي";s:3:"ﱼ";s:4:"فى";s:3:"ﱽ";s:4:"في";s:3:"ﱾ";s:4:"قى";s:3:"ﱿ";s:4:"قي";s:3:"ﲀ";s:4:"كا";s:3:"ﲁ";s:4:"كل";s:3:"ﲂ";s:4:"كم";s:3:"ﲃ";s:4:"كى";s:3:"ﲄ";s:4:"كي";s:3:"ﲅ";s:4:"لم";s:3:"ﲆ";s:4:"لى";s:3:"ﲇ";s:4:"لي";s:3:"ﲈ";s:4:"ما";s:3:"ﲉ";s:4:"مم";s:3:"ﲊ";s:4:"نر";s:3:"ﲋ";s:4:"نز";s:3:"ﲌ";s:4:"نم";s:3:"ﲍ";s:4:"نن";s:3:"ﲎ";s:4:"نى";s:3:"ﲏ";s:4:"ني";s:3:"ﲐ";s:4:"ىٰ";s:3:"ﲑ";s:4:"ير";s:3:"ﲒ";s:4:"يز";s:3:"ﲓ";s:4:"يم";s:3:"ﲔ";s:4:"ين";s:3:"ﲕ";s:4:"يى";s:3:"ﲖ";s:4:"يي";s:3:"ﲗ";s:6:"ئج";s:3:"ﲘ";s:6:"ئح";s:3:"ﲙ";s:6:"ئخ";s:3:"ﲚ";s:6:"ئم";s:3:"ﲛ";s:6:"ئه";s:3:"ﲜ";s:4:"بج";s:3:"ﲝ";s:4:"بح";s:3:"ﲞ";s:4:"بخ";s:3:"ﲟ";s:4:"بم";s:3:"ﲠ";s:4:"به";s:3:"ﲡ";s:4:"تج";s:3:"ﲢ";s:4:"تح";s:3:"ﲣ";s:4:"تخ";s:3:"ﲤ";s:4:"تم";s:3:"ﲥ";s:4:"ته";s:3:"ﲦ";s:4:"ثم";s:3:"ﲧ";s:4:"جح";s:3:"ﲨ";s:4:"جم";s:3:"ﲩ";s:4:"حج";s:3:"ﲪ";s:4:"حم";s:3:"ﲫ";s:4:"خج";s:3:"ﲬ";s:4:"خم";s:3:"ﲭ";s:4:"سج";s:3:"ﲮ";s:4:"سح";s:3:"ﲯ";s:4:"سخ";s:3:"ﲰ";s:4:"سم";s:3:"ﲱ";s:4:"صح";s:3:"ﲲ";s:4:"صخ";s:3:"ﲳ";s:4:"صم";s:3:"ﲴ";s:4:"ضج";s:3:"ﲵ";s:4:"ضح";s:3:"ﲶ";s:4:"ضخ";s:3:"ﲷ";s:4:"ضم";s:3:"ﲸ";s:4:"طح";s:3:"ﲹ";s:4:"ظم";s:3:"ﲺ";s:4:"عج";s:3:"ﲻ";s:4:"عم";s:3:"ﲼ";s:4:"غج";s:3:"ﲽ";s:4:"غم";s:3:"ﲾ";s:4:"فج";s:3:"ﲿ";s:4:"فح";s:3:"ﳀ";s:4:"فخ";s:3:"ﳁ";s:4:"فم";s:3:"ﳂ";s:4:"قح";s:3:"ﳃ";s:4:"قم";s:3:"ﳄ";s:4:"كج";s:3:"ﳅ";s:4:"كح";s:3:"ﳆ";s:4:"كخ";s:3:"ﳇ";s:4:"كل";s:3:"ﳈ";s:4:"كم";s:3:"ﳉ";s:4:"لج";s:3:"ﳊ";s:4:"لح";s:3:"ﳋ";s:4:"لخ";s:3:"ﳌ";s:4:"لم";s:3:"ﳍ";s:4:"له";s:3:"ﳎ";s:4:"مج";s:3:"ﳏ";s:4:"مح";s:3:"ﳐ";s:4:"مخ";s:3:"ﳑ";s:4:"مم";s:3:"ﳒ";s:4:"نج";s:3:"ﳓ";s:4:"نح";s:3:"ﳔ";s:4:"نخ";s:3:"ﳕ";s:4:"نم";s:3:"ﳖ";s:4:"نه";s:3:"ﳗ";s:4:"هج";s:3:"ﳘ";s:4:"هم";s:3:"ﳙ";s:4:"هٰ";s:3:"ﳚ";s:4:"يج";s:3:"ﳛ";s:4:"يح";s:3:"ﳜ";s:4:"يخ";s:3:"ﳝ";s:4:"يم";s:3:"ﳞ";s:4:"يه";s:3:"ﳟ";s:6:"ئم";s:3:"ﳠ";s:6:"ئه";s:3:"ﳡ";s:4:"بم";s:3:"ﳢ";s:4:"به";s:3:"ﳣ";s:4:"تم";s:3:"ﳤ";s:4:"ته";s:3:"ﳥ";s:4:"ثم";s:3:"ﳦ";s:4:"ثه";s:3:"ﳧ";s:4:"سم";s:3:"ﳨ";s:4:"سه";s:3:"ﳩ";s:4:"شم";s:3:"ﳪ";s:4:"شه";s:3:"ﳫ";s:4:"كل";s:3:"ﳬ";s:4:"كم";s:3:"ﳭ";s:4:"لم";s:3:"ﳮ";s:4:"نم";s:3:"ﳯ";s:4:"نه";s:3:"ﳰ";s:4:"يم";s:3:"ﳱ";s:4:"يه";s:3:"ﳲ";s:6:"ـَّ";s:3:"ﳳ";s:6:"ـُّ";s:3:"ﳴ";s:6:"ـِّ";s:3:"ﳵ";s:4:"طى";s:3:"ﳶ";s:4:"طي";s:3:"ﳷ";s:4:"عى";s:3:"ﳸ";s:4:"عي";s:3:"ﳹ";s:4:"غى";s:3:"ﳺ";s:4:"غي";s:3:"ﳻ";s:4:"سى";s:3:"ﳼ";s:4:"سي";s:3:"ﳽ";s:4:"شى";s:3:"ﳾ";s:4:"شي";s:3:"ﳿ";s:4:"حى";s:3:"ﴀ";s:4:"حي";s:3:"ﴁ";s:4:"جى";s:3:"ﴂ";s:4:"جي";s:3:"ﴃ";s:4:"خى";s:3:"ﴄ";s:4:"خي";s:3:"ﴅ";s:4:"صى";s:3:"ﴆ";s:4:"صي";s:3:"ﴇ";s:4:"ضى";s:3:"ﴈ";s:4:"ضي";s:3:"ﴉ";s:4:"شج";s:3:"ﴊ";s:4:"شح";s:3:"ﴋ";s:4:"شخ";s:3:"ﴌ";s:4:"شم";s:3:"ﴍ";s:4:"شر";s:3:"ﴎ";s:4:"سر";s:3:"ﴏ";s:4:"صر";s:3:"ﴐ";s:4:"ضر";s:3:"ﴑ";s:4:"طى";s:3:"ﴒ";s:4:"طي";s:3:"ﴓ";s:4:"عى";s:3:"ﴔ";s:4:"عي";s:3:"ﴕ";s:4:"غى";s:3:"ﴖ";s:4:"غي";s:3:"ﴗ";s:4:"سى";s:3:"ﴘ";s:4:"سي";s:3:"ﴙ";s:4:"شى";s:3:"ﴚ";s:4:"شي";s:3:"ﴛ";s:4:"حى";s:3:"ﴜ";s:4:"حي";s:3:"ﴝ";s:4:"جى";s:3:"ﴞ";s:4:"جي";s:3:"ﴟ";s:4:"خى";s:3:"ﴠ";s:4:"خي";s:3:"ﴡ";s:4:"صى";s:3:"ﴢ";s:4:"صي";s:3:"ﴣ";s:4:"ضى";s:3:"ﴤ";s:4:"ضي";s:3:"ﴥ";s:4:"شج";s:3:"ﴦ";s:4:"شح";s:3:"ﴧ";s:4:"شخ";s:3:"ﴨ";s:4:"شم";s:3:"ﴩ";s:4:"شر";s:3:"ﴪ";s:4:"سر";s:3:"ﴫ";s:4:"صر";s:3:"ﴬ";s:4:"ضر";s:3:"ﴭ";s:4:"شج";s:3:"ﴮ";s:4:"شح";s:3:"ﴯ";s:4:"شخ";s:3:"ﴰ";s:4:"شم";s:3:"ﴱ";s:4:"سه";s:3:"ﴲ";s:4:"شه";s:3:"ﴳ";s:4:"طم";s:3:"ﴴ";s:4:"سج";s:3:"ﴵ";s:4:"سح";s:3:"ﴶ";s:4:"سخ";s:3:"ﴷ";s:4:"شج";s:3:"ﴸ";s:4:"شح";s:3:"ﴹ";s:4:"شخ";s:3:"ﴺ";s:4:"طم";s:3:"ﴻ";s:4:"ظم";s:3:"ﴼ";s:4:"اً";s:3:"ﴽ";s:4:"اً";s:3:"ﵐ";s:6:"تجم";s:3:"ﵑ";s:6:"تحج";s:3:"ﵒ";s:6:"تحج";s:3:"ﵓ";s:6:"تحم";s:3:"ﵔ";s:6:"تخم";s:3:"ﵕ";s:6:"تمج";s:3:"ﵖ";s:6:"تمح";s:3:"ﵗ";s:6:"تمخ";s:3:"ﵘ";s:6:"جمح";s:3:"ﵙ";s:6:"جمح";s:3:"ﵚ";s:6:"حمي";s:3:"ﵛ";s:6:"حمى";s:3:"ﵜ";s:6:"سحج";s:3:"ﵝ";s:6:"سجح";s:3:"ﵞ";s:6:"سجى";s:3:"ﵟ";s:6:"سمح";s:3:"ﵠ";s:6:"سمح";s:3:"ﵡ";s:6:"سمج";s:3:"ﵢ";s:6:"سمم";s:3:"ﵣ";s:6:"سمم";s:3:"ﵤ";s:6:"صحح";s:3:"ﵥ";s:6:"صحح";s:3:"ﵦ";s:6:"صمم";s:3:"ﵧ";s:6:"شحم";s:3:"ﵨ";s:6:"شحم";s:3:"ﵩ";s:6:"شجي";s:3:"ﵪ";s:6:"شمخ";s:3:"ﵫ";s:6:"شمخ";s:3:"ﵬ";s:6:"شمم";s:3:"ﵭ";s:6:"شمم";s:3:"ﵮ";s:6:"ضحى";s:3:"ﵯ";s:6:"ضخم";s:3:"ﵰ";s:6:"ضخم";s:3:"ﵱ";s:6:"طمح";s:3:"ﵲ";s:6:"طمح";s:3:"ﵳ";s:6:"طمم";s:3:"ﵴ";s:6:"طمي";s:3:"ﵵ";s:6:"عجم";s:3:"ﵶ";s:6:"عمم";s:3:"ﵷ";s:6:"عمم";s:3:"ﵸ";s:6:"عمى";s:3:"ﵹ";s:6:"غمم";s:3:"ﵺ";s:6:"غمي";s:3:"ﵻ";s:6:"غمى";s:3:"ﵼ";s:6:"فخم";s:3:"ﵽ";s:6:"فخم";s:3:"ﵾ";s:6:"قمح";s:3:"ﵿ";s:6:"قمم";s:3:"ﶀ";s:6:"لحم";s:3:"ﶁ";s:6:"لحي";s:3:"ﶂ";s:6:"لحى";s:3:"ﶃ";s:6:"لجج";s:3:"ﶄ";s:6:"لجج";s:3:"ﶅ";s:6:"لخم";s:3:"ﶆ";s:6:"لخم";s:3:"ﶇ";s:6:"لمح";s:3:"ﶈ";s:6:"لمح";s:3:"ﶉ";s:6:"محج";s:3:"ﶊ";s:6:"محم";s:3:"ﶋ";s:6:"محي";s:3:"ﶌ";s:6:"مجح";s:3:"ﶍ";s:6:"مجم";s:3:"ﶎ";s:6:"مخج";s:3:"ﶏ";s:6:"مخم";s:3:"ﶒ";s:6:"مجخ";s:3:"ﶓ";s:6:"همج";s:3:"ﶔ";s:6:"همم";s:3:"ﶕ";s:6:"نحم";s:3:"ﶖ";s:6:"نحى";s:3:"ﶗ";s:6:"نجم";s:3:"ﶘ";s:6:"نجم";s:3:"ﶙ";s:6:"نجى";s:3:"ﶚ";s:6:"نمي";s:3:"ﶛ";s:6:"نمى";s:3:"ﶜ";s:6:"يمم";s:3:"ﶝ";s:6:"يمم";s:3:"ﶞ";s:6:"بخي";s:3:"ﶟ";s:6:"تجي";s:3:"ﶠ";s:6:"تجى";s:3:"ﶡ";s:6:"تخي";s:3:"ﶢ";s:6:"تخى";s:3:"ﶣ";s:6:"تمي";s:3:"ﶤ";s:6:"تمى";s:3:"ﶥ";s:6:"جمي";s:3:"ﶦ";s:6:"جحى";s:3:"ﶧ";s:6:"جمى";s:3:"ﶨ";s:6:"سخى";s:3:"ﶩ";s:6:"صحي";s:3:"ﶪ";s:6:"شحي";s:3:"ﶫ";s:6:"ضحي";s:3:"ﶬ";s:6:"لجي";s:3:"ﶭ";s:6:"لمي";s:3:"ﶮ";s:6:"يحي";s:3:"ﶯ";s:6:"يجي";s:3:"ﶰ";s:6:"يمي";s:3:"ﶱ";s:6:"ممي";s:3:"ﶲ";s:6:"قمي";s:3:"ﶳ";s:6:"نحي";s:3:"ﶴ";s:6:"قمح";s:3:"ﶵ";s:6:"لحم";s:3:"ﶶ";s:6:"عمي";s:3:"ﶷ";s:6:"كمي";s:3:"ﶸ";s:6:"نجح";s:3:"ﶹ";s:6:"مخي";s:3:"ﶺ";s:6:"لجم";s:3:"ﶻ";s:6:"كمم";s:3:"ﶼ";s:6:"لجم";s:3:"ﶽ";s:6:"نجح";s:3:"ﶾ";s:6:"جحي";s:3:"ﶿ";s:6:"حجي";s:3:"ﷀ";s:6:"مجي";s:3:"ﷁ";s:6:"فمي";s:3:"ﷂ";s:6:"بحي";s:3:"ﷃ";s:6:"كمم";s:3:"ﷄ";s:6:"عجم";s:3:"ﷅ";s:6:"صمم";s:3:"ﷆ";s:6:"سخي";s:3:"ﷇ";s:6:"نجي";s:3:"ﷰ";s:6:"صلے";s:3:"ﷱ";s:6:"قلے";s:3:"ﷲ";s:8:"الله";s:3:"ﷳ";s:8:"اكبر";s:3:"ﷴ";s:8:"محمد";s:3:"ﷵ";s:8:"صلعم";s:3:"ﷶ";s:8:"رسول";s:3:"ﷷ";s:8:"عليه";s:3:"ﷸ";s:8:"وسلم";s:3:"ﷹ";s:6:"صلى";s:3:"ﷺ";s:33:"صلى الله عليه وسلم";s:3:"ﷻ";s:15:"جل جلاله";s:3:"﷼";s:8:"ریال";s:3:"︐";s:1:",";s:3:"︑";s:3:"、";s:3:"︒";s:3:"。";s:3:"︓";s:1:":";s:3:"︔";s:1:";";s:3:"︕";s:1:"!";s:3:"︖";s:1:"?";s:3:"︗";s:3:"〖";s:3:"︘";s:3:"〗";s:3:"︙";s:3:"...";s:3:"︰";s:2:"..";s:3:"︱";s:3:"—";s:3:"︲";s:3:"–";s:3:"︳";s:1:"_";s:3:"︴";s:1:"_";s:3:"︵";s:1:"(";s:3:"︶";s:1:")";s:3:"︷";s:1:"{";s:3:"︸";s:1:"}";s:3:"︹";s:3:"〔";s:3:"︺";s:3:"〕";s:3:"︻";s:3:"【";s:3:"︼";s:3:"】";s:3:"︽";s:3:"《";s:3:"︾";s:3:"》";s:3:"︿";s:3:"〈";s:3:"﹀";s:3:"〉";s:3:"﹁";s:3:"「";s:3:"﹂";s:3:"」";s:3:"﹃";s:3:"『";s:3:"﹄";s:3:"』";s:3:"﹇";s:1:"[";s:3:"﹈";s:1:"]";s:3:"﹉";s:3:" ̅";s:3:"﹊";s:3:" ̅";s:3:"﹋";s:3:" ̅";s:3:"﹌";s:3:" ̅";s:3:"﹍";s:1:"_";s:3:"﹎";s:1:"_";s:3:"﹏";s:1:"_";s:3:"﹐";s:1:",";s:3:"﹑";s:3:"、";s:3:"﹒";s:1:".";s:3:"﹔";s:1:";";s:3:"﹕";s:1:":";s:3:"﹖";s:1:"?";s:3:"﹗";s:1:"!";s:3:"﹘";s:3:"—";s:3:"﹙";s:1:"(";s:3:"﹚";s:1:")";s:3:"﹛";s:1:"{";s:3:"﹜";s:1:"}";s:3:"﹝";s:3:"〔";s:3:"﹞";s:3:"〕";s:3:"﹟";s:1:"#";s:3:"﹠";s:1:"&";s:3:"﹡";s:1:"*";s:3:"﹢";s:1:"+";s:3:"﹣";s:1:"-";s:3:"﹤";s:1:"<";s:3:"﹥";s:1:">";s:3:"﹦";s:1:"=";s:3:"﹨";s:1:"\\";s:3:"﹩";s:1:"$";s:3:"﹪";s:1:"%";s:3:"﹫";s:1:"@";s:3:"ﹰ";s:3:" ً";s:3:"ﹱ";s:4:"ـً";s:3:"ﹲ";s:3:" ٌ";s:3:"ﹴ";s:3:" ٍ";s:3:"ﹶ";s:3:" َ";s:3:"ﹷ";s:4:"ـَ";s:3:"ﹸ";s:3:" ُ";s:3:"ﹹ";s:4:"ـُ";s:3:"ﹺ";s:3:" ِ";s:3:"ﹻ";s:4:"ـِ";s:3:"ﹼ";s:3:" ّ";s:3:"ﹽ";s:4:"ـّ";s:3:"ﹾ";s:3:" ْ";s:3:"ﹿ";s:4:"ـْ";s:3:"ﺀ";s:2:"ء";s:3:"ﺁ";s:4:"آ";s:3:"ﺂ";s:4:"آ";s:3:"ﺃ";s:4:"أ";s:3:"ﺄ";s:4:"أ";s:3:"ﺅ";s:4:"ؤ";s:3:"ﺆ";s:4:"ؤ";s:3:"ﺇ";s:4:"إ";s:3:"ﺈ";s:4:"إ";s:3:"ﺉ";s:4:"ئ";s:3:"ﺊ";s:4:"ئ";s:3:"ﺋ";s:4:"ئ";s:3:"ﺌ";s:4:"ئ";s:3:"ﺍ";s:2:"ا";s:3:"ﺎ";s:2:"ا";s:3:"ﺏ";s:2:"ب";s:3:"ﺐ";s:2:"ب";s:3:"ﺑ";s:2:"ب";s:3:"ﺒ";s:2:"ب";s:3:"ﺓ";s:2:"ة";s:3:"ﺔ";s:2:"ة";s:3:"ﺕ";s:2:"ت";s:3:"ﺖ";s:2:"ت";s:3:"ﺗ";s:2:"ت";s:3:"ﺘ";s:2:"ت";s:3:"ﺙ";s:2:"ث";s:3:"ﺚ";s:2:"ث";s:3:"ﺛ";s:2:"ث";s:3:"ﺜ";s:2:"ث";s:3:"ﺝ";s:2:"ج";s:3:"ﺞ";s:2:"ج";s:3:"ﺟ";s:2:"ج";s:3:"ﺠ";s:2:"ج";s:3:"ﺡ";s:2:"ح";s:3:"ﺢ";s:2:"ح";s:3:"ﺣ";s:2:"ح";s:3:"ﺤ";s:2:"ح";s:3:"ﺥ";s:2:"خ";s:3:"ﺦ";s:2:"خ";s:3:"ﺧ";s:2:"خ";s:3:"ﺨ";s:2:"خ";s:3:"ﺩ";s:2:"د";s:3:"ﺪ";s:2:"د";s:3:"ﺫ";s:2:"ذ";s:3:"ﺬ";s:2:"ذ";s:3:"ﺭ";s:2:"ر";s:3:"ﺮ";s:2:"ر";s:3:"ﺯ";s:2:"ز";s:3:"ﺰ";s:2:"ز";s:3:"ﺱ";s:2:"س";s:3:"ﺲ";s:2:"س";s:3:"ﺳ";s:2:"س";s:3:"ﺴ";s:2:"س";s:3:"ﺵ";s:2:"ش";s:3:"ﺶ";s:2:"ش";s:3:"ﺷ";s:2:"ش";s:3:"ﺸ";s:2:"ش";s:3:"ﺹ";s:2:"ص";s:3:"ﺺ";s:2:"ص";s:3:"ﺻ";s:2:"ص";s:3:"ﺼ";s:2:"ص";s:3:"ﺽ";s:2:"ض";s:3:"ﺾ";s:2:"ض";s:3:"ﺿ";s:2:"ض";s:3:"ﻀ";s:2:"ض";s:3:"ﻁ";s:2:"ط";s:3:"ﻂ";s:2:"ط";s:3:"ﻃ";s:2:"ط";s:3:"ﻄ";s:2:"ط";s:3:"ﻅ";s:2:"ظ";s:3:"ﻆ";s:2:"ظ";s:3:"ﻇ";s:2:"ظ";s:3:"ﻈ";s:2:"ظ";s:3:"ﻉ";s:2:"ع";s:3:"ﻊ";s:2:"ع";s:3:"ﻋ";s:2:"ع";s:3:"ﻌ";s:2:"ع";s:3:"ﻍ";s:2:"غ";s:3:"ﻎ";s:2:"غ";s:3:"ﻏ";s:2:"غ";s:3:"ﻐ";s:2:"غ";s:3:"ﻑ";s:2:"ف";s:3:"ﻒ";s:2:"ف";s:3:"ﻓ";s:2:"ف";s:3:"ﻔ";s:2:"ف";s:3:"ﻕ";s:2:"ق";s:3:"ﻖ";s:2:"ق";s:3:"ﻗ";s:2:"ق";s:3:"ﻘ";s:2:"ق";s:3:"ﻙ";s:2:"ك";s:3:"ﻚ";s:2:"ك";s:3:"ﻛ";s:2:"ك";s:3:"ﻜ";s:2:"ك";s:3:"ﻝ";s:2:"ل";s:3:"ﻞ";s:2:"ل";s:3:"ﻟ";s:2:"ل";s:3:"ﻠ";s:2:"ل";s:3:"ﻡ";s:2:"م";s:3:"ﻢ";s:2:"م";s:3:"ﻣ";s:2:"م";s:3:"ﻤ";s:2:"م";s:3:"ﻥ";s:2:"ن";s:3:"ﻦ";s:2:"ن";s:3:"ﻧ";s:2:"ن";s:3:"ﻨ";s:2:"ن";s:3:"ﻩ";s:2:"ه";s:3:"ﻪ";s:2:"ه";s:3:"ﻫ";s:2:"ه";s:3:"ﻬ";s:2:"ه";s:3:"ﻭ";s:2:"و";s:3:"ﻮ";s:2:"و";s:3:"ﻯ";s:2:"ى";s:3:"ﻰ";s:2:"ى";s:3:"ﻱ";s:2:"ي";s:3:"ﻲ";s:2:"ي";s:3:"ﻳ";s:2:"ي";s:3:"ﻴ";s:2:"ي";s:3:"ﻵ";s:6:"لآ";s:3:"ﻶ";s:6:"لآ";s:3:"ﻷ";s:6:"لأ";s:3:"ﻸ";s:6:"لأ";s:3:"ﻹ";s:6:"لإ";s:3:"ﻺ";s:6:"لإ";s:3:"ﻻ";s:4:"لا";s:3:"ﻼ";s:4:"لا";s:3:"!";s:1:"!";s:3:""";s:1:""";s:3:"#";s:1:"#";s:3:"$";s:1:"$";s:3:"%";s:1:"%";s:3:"&";s:1:"&";s:3:"'";s:1:"\'";s:3:"(";s:1:"(";s:3:")";s:1:")";s:3:"*";s:1:"*";s:3:"+";s:1:"+";s:3:",";s:1:",";s:3:"-";s:1:"-";s:3:".";s:1:".";s:3:"/";s:1:"/";s:3:"0";s:1:"0";s:3:"1";s:1:"1";s:3:"2";s:1:"2";s:3:"3";s:1:"3";s:3:"4";s:1:"4";s:3:"5";s:1:"5";s:3:"6";s:1:"6";s:3:"7";s:1:"7";s:3:"8";s:1:"8";s:3:"9";s:1:"9";s:3:":";s:1:":";s:3:";";s:1:";";s:3:"<";s:1:"<";s:3:"=";s:1:"=";s:3:">";s:1:">";s:3:"?";s:1:"?";s:3:"@";s:1:"@";s:3:"A";s:1:"A";s:3:"B";s:1:"B";s:3:"C";s:1:"C";s:3:"D";s:1:"D";s:3:"E";s:1:"E";s:3:"F";s:1:"F";s:3:"G";s:1:"G";s:3:"H";s:1:"H";s:3:"I";s:1:"I";s:3:"J";s:1:"J";s:3:"K";s:1:"K";s:3:"L";s:1:"L";s:3:"M";s:1:"M";s:3:"N";s:1:"N";s:3:"O";s:1:"O";s:3:"P";s:1:"P";s:3:"Q";s:1:"Q";s:3:"R";s:1:"R";s:3:"S";s:1:"S";s:3:"T";s:1:"T";s:3:"U";s:1:"U";s:3:"V";s:1:"V";s:3:"W";s:1:"W";s:3:"X";s:1:"X";s:3:"Y";s:1:"Y";s:3:"Z";s:1:"Z";s:3:"[";s:1:"[";s:3:"\";s:1:"\\";s:3:"]";s:1:"]";s:3:"^";s:1:"^";s:3:"_";s:1:"_";s:3:"`";s:1:"`";s:3:"a";s:1:"a";s:3:"b";s:1:"b";s:3:"c";s:1:"c";s:3:"d";s:1:"d";s:3:"e";s:1:"e";s:3:"f";s:1:"f";s:3:"g";s:1:"g";s:3:"h";s:1:"h";s:3:"i";s:1:"i";s:3:"j";s:1:"j";s:3:"k";s:1:"k";s:3:"l";s:1:"l";s:3:"m";s:1:"m";s:3:"n";s:1:"n";s:3:"o";s:1:"o";s:3:"p";s:1:"p";s:3:"q";s:1:"q";s:3:"r";s:1:"r";s:3:"s";s:1:"s";s:3:"t";s:1:"t";s:3:"u";s:1:"u";s:3:"v";s:1:"v";s:3:"w";s:1:"w";s:3:"x";s:1:"x";s:3:"y";s:1:"y";s:3:"z";s:1:"z";s:3:"{";s:1:"{";s:3:"|";s:1:"|";s:3:"}";s:1:"}";s:3:"~";s:1:"~";s:3:"⦅";s:3:"⦅";s:3:"⦆";s:3:"⦆";s:3:"。";s:3:"。";s:3:"「";s:3:"「";s:3:"」";s:3:"」";s:3:"、";s:3:"、";s:3:"・";s:3:"・";s:3:"ヲ";s:3:"ヲ";s:3:"ァ";s:3:"ァ";s:3:"ィ";s:3:"ィ";s:3:"ゥ";s:3:"ゥ";s:3:"ェ";s:3:"ェ";s:3:"ォ";s:3:"ォ";s:3:"ャ";s:3:"ャ";s:3:"ュ";s:3:"ュ";s:3:"ョ";s:3:"ョ";s:3:"ッ";s:3:"ッ";s:3:"ー";s:3:"ー";s:3:"ア";s:3:"ア";s:3:"イ";s:3:"イ";s:3:"ウ";s:3:"ウ";s:3:"エ";s:3:"エ";s:3:"オ";s:3:"オ";s:3:"カ";s:3:"カ";s:3:"キ";s:3:"キ";s:3:"ク";s:3:"ク";s:3:"ケ";s:3:"ケ";s:3:"コ";s:3:"コ";s:3:"サ";s:3:"サ";s:3:"シ";s:3:"シ";s:3:"ス";s:3:"ス";s:3:"セ";s:3:"セ";s:3:"ソ";s:3:"ソ";s:3:"タ";s:3:"タ";s:3:"チ";s:3:"チ";s:3:"ツ";s:3:"ツ";s:3:"テ";s:3:"テ";s:3:"ト";s:3:"ト";s:3:"ナ";s:3:"ナ";s:3:"ニ";s:3:"ニ";s:3:"ヌ";s:3:"ヌ";s:3:"ネ";s:3:"ネ";s:3:"ノ";s:3:"ノ";s:3:"ハ";s:3:"ハ";s:3:"ヒ";s:3:"ヒ";s:3:"フ";s:3:"フ";s:3:"ヘ";s:3:"ヘ";s:3:"ホ";s:3:"ホ";s:3:"マ";s:3:"マ";s:3:"ミ";s:3:"ミ";s:3:"ム";s:3:"ム";s:3:"メ";s:3:"メ";s:3:"モ";s:3:"モ";s:3:"ヤ";s:3:"ヤ";s:3:"ユ";s:3:"ユ";s:3:"ヨ";s:3:"ヨ";s:3:"ラ";s:3:"ラ";s:3:"リ";s:3:"リ";s:3:"ル";s:3:"ル";s:3:"レ";s:3:"レ";s:3:"ロ";s:3:"ロ";s:3:"ワ";s:3:"ワ";s:3:"ン";s:3:"ン";s:3:"゙";s:3:"゙";s:3:"゚";s:3:"゚";s:3:"ᅠ";s:3:"ᅠ";s:3:"ᄀ";s:3:"ᄀ";s:3:"ᄁ";s:3:"ᄁ";s:3:"ᆪ";s:3:"ᆪ";s:3:"ᄂ";s:3:"ᄂ";s:3:"ᆬ";s:3:"ᆬ";s:3:"ᆭ";s:3:"ᆭ";s:3:"ᄃ";s:3:"ᄃ";s:3:"ᄄ";s:3:"ᄄ";s:3:"ᄅ";s:3:"ᄅ";s:3:"ᆰ";s:3:"ᆰ";s:3:"ᆱ";s:3:"ᆱ";s:3:"ᆲ";s:3:"ᆲ";s:3:"ᆳ";s:3:"ᆳ";s:3:"ᆴ";s:3:"ᆴ";s:3:"ᆵ";s:3:"ᆵ";s:3:"ᄚ";s:3:"ᄚ";s:3:"ᄆ";s:3:"ᄆ";s:3:"ᄇ";s:3:"ᄇ";s:3:"ᄈ";s:3:"ᄈ";s:3:"ᄡ";s:3:"ᄡ";s:3:"ᄉ";s:3:"ᄉ";s:3:"ᄊ";s:3:"ᄊ";s:3:"ᄋ";s:3:"ᄋ";s:3:"ᄌ";s:3:"ᄌ";s:3:"ᄍ";s:3:"ᄍ";s:3:"ᄎ";s:3:"ᄎ";s:3:"ᄏ";s:3:"ᄏ";s:3:"ᄐ";s:3:"ᄐ";s:3:"ᄑ";s:3:"ᄑ";s:3:"ᄒ";s:3:"ᄒ";s:3:"ᅡ";s:3:"ᅡ";s:3:"ᅢ";s:3:"ᅢ";s:3:"ᅣ";s:3:"ᅣ";s:3:"ᅤ";s:3:"ᅤ";s:3:"ᅥ";s:3:"ᅥ";s:3:"ᅦ";s:3:"ᅦ";s:3:"ᅧ";s:3:"ᅧ";s:3:"ᅨ";s:3:"ᅨ";s:3:"ᅩ";s:3:"ᅩ";s:3:"ᅪ";s:3:"ᅪ";s:3:"ᅫ";s:3:"ᅫ";s:3:"ᅬ";s:3:"ᅬ";s:3:"ᅭ";s:3:"ᅭ";s:3:"ᅮ";s:3:"ᅮ";s:3:"ᅯ";s:3:"ᅯ";s:3:"ᅰ";s:3:"ᅰ";s:3:"ᅱ";s:3:"ᅱ";s:3:"ᅲ";s:3:"ᅲ";s:3:"ᅳ";s:3:"ᅳ";s:3:"ᅴ";s:3:"ᅴ";s:3:"ᅵ";s:3:"ᅵ";s:3:"¢";s:2:"¢";s:3:"£";s:2:"£";s:3:"¬";s:2:"¬";s:3:" ̄";s:3:" ̄";s:3:"¦";s:2:"¦";s:3:"¥";s:2:"¥";s:3:"₩";s:3:"₩";s:3:"│";s:3:"│";s:3:"←";s:3:"←";s:3:"↑";s:3:"↑";s:3:"→";s:3:"→";s:3:"↓";s:3:"↓";s:3:"■";s:3:"■";s:3:"○";s:3:"○";s:4:"𝅗𝅥";s:8:"𝅗𝅥";s:4:"𝅘𝅥";s:8:"𝅘𝅥";s:4:"𝅘𝅥𝅮";s:12:"𝅘𝅥𝅮";s:4:"𝅘𝅥𝅯";s:12:"𝅘𝅥𝅯";s:4:"𝅘𝅥𝅰";s:12:"𝅘𝅥𝅰";s:4:"𝅘𝅥𝅱";s:12:"𝅘𝅥𝅱";s:4:"𝅘𝅥𝅲";s:12:"𝅘𝅥𝅲";s:4:"𝆹𝅥";s:8:"𝆹𝅥";s:4:"𝆺𝅥";s:8:"𝆺𝅥";s:4:"𝆹𝅥𝅮";s:12:"𝆹𝅥𝅮";s:4:"𝆺𝅥𝅮";s:12:"𝆺𝅥𝅮";s:4:"𝆹𝅥𝅯";s:12:"𝆹𝅥𝅯";s:4:"𝆺𝅥𝅯";s:12:"𝆺𝅥𝅯";s:4:"𝐀";s:1:"A";s:4:"𝐁";s:1:"B";s:4:"𝐂";s:1:"C";s:4:"𝐃";s:1:"D";s:4:"𝐄";s:1:"E";s:4:"𝐅";s:1:"F";s:4:"𝐆";s:1:"G";s:4:"𝐇";s:1:"H";s:4:"𝐈";s:1:"I";s:4:"𝐉";s:1:"J";s:4:"𝐊";s:1:"K";s:4:"𝐋";s:1:"L";s:4:"𝐌";s:1:"M";s:4:"𝐍";s:1:"N";s:4:"𝐎";s:1:"O";s:4:"𝐏";s:1:"P";s:4:"𝐐";s:1:"Q";s:4:"𝐑";s:1:"R";s:4:"𝐒";s:1:"S";s:4:"𝐓";s:1:"T";s:4:"𝐔";s:1:"U";s:4:"𝐕";s:1:"V";s:4:"𝐖";s:1:"W";s:4:"𝐗";s:1:"X";s:4:"𝐘";s:1:"Y";s:4:"𝐙";s:1:"Z";s:4:"𝐚";s:1:"a";s:4:"𝐛";s:1:"b";s:4:"𝐜";s:1:"c";s:4:"𝐝";s:1:"d";s:4:"𝐞";s:1:"e";s:4:"𝐟";s:1:"f";s:4:"𝐠";s:1:"g";s:4:"𝐡";s:1:"h";s:4:"𝐢";s:1:"i";s:4:"𝐣";s:1:"j";s:4:"𝐤";s:1:"k";s:4:"𝐥";s:1:"l";s:4:"𝐦";s:1:"m";s:4:"𝐧";s:1:"n";s:4:"𝐨";s:1:"o";s:4:"𝐩";s:1:"p";s:4:"𝐪";s:1:"q";s:4:"𝐫";s:1:"r";s:4:"𝐬";s:1:"s";s:4:"𝐭";s:1:"t";s:4:"𝐮";s:1:"u";s:4:"𝐯";s:1:"v";s:4:"𝐰";s:1:"w";s:4:"𝐱";s:1:"x";s:4:"𝐲";s:1:"y";s:4:"𝐳";s:1:"z";s:4:"𝐴";s:1:"A";s:4:"𝐵";s:1:"B";s:4:"𝐶";s:1:"C";s:4:"𝐷";s:1:"D";s:4:"𝐸";s:1:"E";s:4:"𝐹";s:1:"F";s:4:"𝐺";s:1:"G";s:4:"𝐻";s:1:"H";s:4:"𝐼";s:1:"I";s:4:"𝐽";s:1:"J";s:4:"𝐾";s:1:"K";s:4:"𝐿";s:1:"L";s:4:"𝑀";s:1:"M";s:4:"𝑁";s:1:"N";s:4:"𝑂";s:1:"O";s:4:"𝑃";s:1:"P";s:4:"𝑄";s:1:"Q";s:4:"𝑅";s:1:"R";s:4:"𝑆";s:1:"S";s:4:"𝑇";s:1:"T";s:4:"𝑈";s:1:"U";s:4:"𝑉";s:1:"V";s:4:"𝑊";s:1:"W";s:4:"𝑋";s:1:"X";s:4:"𝑌";s:1:"Y";s:4:"𝑍";s:1:"Z";s:4:"𝑎";s:1:"a";s:4:"𝑏";s:1:"b";s:4:"𝑐";s:1:"c";s:4:"𝑑";s:1:"d";s:4:"𝑒";s:1:"e";s:4:"𝑓";s:1:"f";s:4:"𝑔";s:1:"g";s:4:"𝑖";s:1:"i";s:4:"𝑗";s:1:"j";s:4:"𝑘";s:1:"k";s:4:"𝑙";s:1:"l";s:4:"𝑚";s:1:"m";s:4:"𝑛";s:1:"n";s:4:"𝑜";s:1:"o";s:4:"𝑝";s:1:"p";s:4:"𝑞";s:1:"q";s:4:"𝑟";s:1:"r";s:4:"𝑠";s:1:"s";s:4:"𝑡";s:1:"t";s:4:"𝑢";s:1:"u";s:4:"𝑣";s:1:"v";s:4:"𝑤";s:1:"w";s:4:"𝑥";s:1:"x";s:4:"𝑦";s:1:"y";s:4:"𝑧";s:1:"z";s:4:"𝑨";s:1:"A";s:4:"𝑩";s:1:"B";s:4:"𝑪";s:1:"C";s:4:"𝑫";s:1:"D";s:4:"𝑬";s:1:"E";s:4:"𝑭";s:1:"F";s:4:"𝑮";s:1:"G";s:4:"𝑯";s:1:"H";s:4:"𝑰";s:1:"I";s:4:"𝑱";s:1:"J";s:4:"𝑲";s:1:"K";s:4:"𝑳";s:1:"L";s:4:"𝑴";s:1:"M";s:4:"𝑵";s:1:"N";s:4:"𝑶";s:1:"O";s:4:"𝑷";s:1:"P";s:4:"𝑸";s:1:"Q";s:4:"𝑹";s:1:"R";s:4:"𝑺";s:1:"S";s:4:"𝑻";s:1:"T";s:4:"𝑼";s:1:"U";s:4:"𝑽";s:1:"V";s:4:"𝑾";s:1:"W";s:4:"𝑿";s:1:"X";s:4:"𝒀";s:1:"Y";s:4:"𝒁";s:1:"Z";s:4:"𝒂";s:1:"a";s:4:"𝒃";s:1:"b";s:4:"𝒄";s:1:"c";s:4:"𝒅";s:1:"d";s:4:"𝒆";s:1:"e";s:4:"𝒇";s:1:"f";s:4:"𝒈";s:1:"g";s:4:"𝒉";s:1:"h";s:4:"𝒊";s:1:"i";s:4:"𝒋";s:1:"j";s:4:"𝒌";s:1:"k";s:4:"𝒍";s:1:"l";s:4:"𝒎";s:1:"m";s:4:"𝒏";s:1:"n";s:4:"𝒐";s:1:"o";s:4:"𝒑";s:1:"p";s:4:"𝒒";s:1:"q";s:4:"𝒓";s:1:"r";s:4:"𝒔";s:1:"s";s:4:"𝒕";s:1:"t";s:4:"𝒖";s:1:"u";s:4:"𝒗";s:1:"v";s:4:"𝒘";s:1:"w";s:4:"𝒙";s:1:"x";s:4:"𝒚";s:1:"y";s:4:"𝒛";s:1:"z";s:4:"𝒜";s:1:"A";s:4:"𝒞";s:1:"C";s:4:"𝒟";s:1:"D";s:4:"𝒢";s:1:"G";s:4:"𝒥";s:1:"J";s:4:"𝒦";s:1:"K";s:4:"𝒩";s:1:"N";s:4:"𝒪";s:1:"O";s:4:"𝒫";s:1:"P";s:4:"𝒬";s:1:"Q";s:4:"𝒮";s:1:"S";s:4:"𝒯";s:1:"T";s:4:"𝒰";s:1:"U";s:4:"𝒱";s:1:"V";s:4:"𝒲";s:1:"W";s:4:"𝒳";s:1:"X";s:4:"𝒴";s:1:"Y";s:4:"𝒵";s:1:"Z";s:4:"𝒶";s:1:"a";s:4:"𝒷";s:1:"b";s:4:"𝒸";s:1:"c";s:4:"𝒹";s:1:"d";s:4:"𝒻";s:1:"f";s:4:"𝒽";s:1:"h";s:4:"𝒾";s:1:"i";s:4:"𝒿";s:1:"j";s:4:"𝓀";s:1:"k";s:4:"𝓁";s:1:"l";s:4:"𝓂";s:1:"m";s:4:"𝓃";s:1:"n";s:4:"𝓅";s:1:"p";s:4:"𝓆";s:1:"q";s:4:"𝓇";s:1:"r";s:4:"𝓈";s:1:"s";s:4:"𝓉";s:1:"t";s:4:"𝓊";s:1:"u";s:4:"𝓋";s:1:"v";s:4:"𝓌";s:1:"w";s:4:"𝓍";s:1:"x";s:4:"𝓎";s:1:"y";s:4:"𝓏";s:1:"z";s:4:"𝓐";s:1:"A";s:4:"𝓑";s:1:"B";s:4:"𝓒";s:1:"C";s:4:"𝓓";s:1:"D";s:4:"𝓔";s:1:"E";s:4:"𝓕";s:1:"F";s:4:"𝓖";s:1:"G";s:4:"𝓗";s:1:"H";s:4:"𝓘";s:1:"I";s:4:"𝓙";s:1:"J";s:4:"𝓚";s:1:"K";s:4:"𝓛";s:1:"L";s:4:"𝓜";s:1:"M";s:4:"𝓝";s:1:"N";s:4:"𝓞";s:1:"O";s:4:"𝓟";s:1:"P";s:4:"𝓠";s:1:"Q";s:4:"𝓡";s:1:"R";s:4:"𝓢";s:1:"S";s:4:"𝓣";s:1:"T";s:4:"𝓤";s:1:"U";s:4:"𝓥";s:1:"V";s:4:"𝓦";s:1:"W";s:4:"𝓧";s:1:"X";s:4:"𝓨";s:1:"Y";s:4:"𝓩";s:1:"Z";s:4:"𝓪";s:1:"a";s:4:"𝓫";s:1:"b";s:4:"𝓬";s:1:"c";s:4:"𝓭";s:1:"d";s:4:"𝓮";s:1:"e";s:4:"𝓯";s:1:"f";s:4:"𝓰";s:1:"g";s:4:"𝓱";s:1:"h";s:4:"𝓲";s:1:"i";s:4:"𝓳";s:1:"j";s:4:"𝓴";s:1:"k";s:4:"𝓵";s:1:"l";s:4:"𝓶";s:1:"m";s:4:"𝓷";s:1:"n";s:4:"𝓸";s:1:"o";s:4:"𝓹";s:1:"p";s:4:"𝓺";s:1:"q";s:4:"𝓻";s:1:"r";s:4:"𝓼";s:1:"s";s:4:"𝓽";s:1:"t";s:4:"𝓾";s:1:"u";s:4:"𝓿";s:1:"v";s:4:"𝔀";s:1:"w";s:4:"𝔁";s:1:"x";s:4:"𝔂";s:1:"y";s:4:"𝔃";s:1:"z";s:4:"𝔄";s:1:"A";s:4:"𝔅";s:1:"B";s:4:"𝔇";s:1:"D";s:4:"𝔈";s:1:"E";s:4:"𝔉";s:1:"F";s:4:"𝔊";s:1:"G";s:4:"𝔍";s:1:"J";s:4:"𝔎";s:1:"K";s:4:"𝔏";s:1:"L";s:4:"𝔐";s:1:"M";s:4:"𝔑";s:1:"N";s:4:"𝔒";s:1:"O";s:4:"𝔓";s:1:"P";s:4:"𝔔";s:1:"Q";s:4:"𝔖";s:1:"S";s:4:"𝔗";s:1:"T";s:4:"𝔘";s:1:"U";s:4:"𝔙";s:1:"V";s:4:"𝔚";s:1:"W";s:4:"𝔛";s:1:"X";s:4:"𝔜";s:1:"Y";s:4:"𝔞";s:1:"a";s:4:"𝔟";s:1:"b";s:4:"𝔠";s:1:"c";s:4:"𝔡";s:1:"d";s:4:"𝔢";s:1:"e";s:4:"𝔣";s:1:"f";s:4:"𝔤";s:1:"g";s:4:"𝔥";s:1:"h";s:4:"𝔦";s:1:"i";s:4:"𝔧";s:1:"j";s:4:"𝔨";s:1:"k";s:4:"𝔩";s:1:"l";s:4:"𝔪";s:1:"m";s:4:"𝔫";s:1:"n";s:4:"𝔬";s:1:"o";s:4:"𝔭";s:1:"p";s:4:"𝔮";s:1:"q";s:4:"𝔯";s:1:"r";s:4:"𝔰";s:1:"s";s:4:"𝔱";s:1:"t";s:4:"𝔲";s:1:"u";s:4:"𝔳";s:1:"v";s:4:"𝔴";s:1:"w";s:4:"𝔵";s:1:"x";s:4:"𝔶";s:1:"y";s:4:"𝔷";s:1:"z";s:4:"𝔸";s:1:"A";s:4:"𝔹";s:1:"B";s:4:"𝔻";s:1:"D";s:4:"𝔼";s:1:"E";s:4:"𝔽";s:1:"F";s:4:"𝔾";s:1:"G";s:4:"𝕀";s:1:"I";s:4:"𝕁";s:1:"J";s:4:"𝕂";s:1:"K";s:4:"𝕃";s:1:"L";s:4:"𝕄";s:1:"M";s:4:"𝕆";s:1:"O";s:4:"𝕊";s:1:"S";s:4:"𝕋";s:1:"T";s:4:"𝕌";s:1:"U";s:4:"𝕍";s:1:"V";s:4:"𝕎";s:1:"W";s:4:"𝕏";s:1:"X";s:4:"𝕐";s:1:"Y";s:4:"𝕒";s:1:"a";s:4:"𝕓";s:1:"b";s:4:"𝕔";s:1:"c";s:4:"𝕕";s:1:"d";s:4:"𝕖";s:1:"e";s:4:"𝕗";s:1:"f";s:4:"𝕘";s:1:"g";s:4:"𝕙";s:1:"h";s:4:"𝕚";s:1:"i";s:4:"𝕛";s:1:"j";s:4:"𝕜";s:1:"k";s:4:"𝕝";s:1:"l";s:4:"𝕞";s:1:"m";s:4:"𝕟";s:1:"n";s:4:"𝕠";s:1:"o";s:4:"𝕡";s:1:"p";s:4:"𝕢";s:1:"q";s:4:"𝕣";s:1:"r";s:4:"𝕤";s:1:"s";s:4:"𝕥";s:1:"t";s:4:"𝕦";s:1:"u";s:4:"𝕧";s:1:"v";s:4:"𝕨";s:1:"w";s:4:"𝕩";s:1:"x";s:4:"𝕪";s:1:"y";s:4:"𝕫";s:1:"z";s:4:"𝕬";s:1:"A";s:4:"𝕭";s:1:"B";s:4:"𝕮";s:1:"C";s:4:"𝕯";s:1:"D";s:4:"𝕰";s:1:"E";s:4:"𝕱";s:1:"F";s:4:"𝕲";s:1:"G";s:4:"𝕳";s:1:"H";s:4:"𝕴";s:1:"I";s:4:"𝕵";s:1:"J";s:4:"𝕶";s:1:"K";s:4:"𝕷";s:1:"L";s:4:"𝕸";s:1:"M";s:4:"𝕹";s:1:"N";s:4:"𝕺";s:1:"O";s:4:"𝕻";s:1:"P";s:4:"𝕼";s:1:"Q";s:4:"𝕽";s:1:"R";s:4:"𝕾";s:1:"S";s:4:"𝕿";s:1:"T";s:4:"𝖀";s:1:"U";s:4:"𝖁";s:1:"V";s:4:"𝖂";s:1:"W";s:4:"𝖃";s:1:"X";s:4:"𝖄";s:1:"Y";s:4:"𝖅";s:1:"Z";s:4:"𝖆";s:1:"a";s:4:"𝖇";s:1:"b";s:4:"𝖈";s:1:"c";s:4:"𝖉";s:1:"d";s:4:"𝖊";s:1:"e";s:4:"𝖋";s:1:"f";s:4:"𝖌";s:1:"g";s:4:"𝖍";s:1:"h";s:4:"𝖎";s:1:"i";s:4:"𝖏";s:1:"j";s:4:"𝖐";s:1:"k";s:4:"𝖑";s:1:"l";s:4:"𝖒";s:1:"m";s:4:"𝖓";s:1:"n";s:4:"𝖔";s:1:"o";s:4:"𝖕";s:1:"p";s:4:"𝖖";s:1:"q";s:4:"𝖗";s:1:"r";s:4:"𝖘";s:1:"s";s:4:"𝖙";s:1:"t";s:4:"𝖚";s:1:"u";s:4:"𝖛";s:1:"v";s:4:"𝖜";s:1:"w";s:4:"𝖝";s:1:"x";s:4:"𝖞";s:1:"y";s:4:"𝖟";s:1:"z";s:4:"𝖠";s:1:"A";s:4:"𝖡";s:1:"B";s:4:"𝖢";s:1:"C";s:4:"𝖣";s:1:"D";s:4:"𝖤";s:1:"E";s:4:"𝖥";s:1:"F";s:4:"𝖦";s:1:"G";s:4:"𝖧";s:1:"H";s:4:"𝖨";s:1:"I";s:4:"𝖩";s:1:"J";s:4:"𝖪";s:1:"K";s:4:"𝖫";s:1:"L";s:4:"𝖬";s:1:"M";s:4:"𝖭";s:1:"N";s:4:"𝖮";s:1:"O";s:4:"𝖯";s:1:"P";s:4:"𝖰";s:1:"Q";s:4:"𝖱";s:1:"R";s:4:"𝖲";s:1:"S";s:4:"𝖳";s:1:"T";s:4:"𝖴";s:1:"U";s:4:"𝖵";s:1:"V";s:4:"𝖶";s:1:"W";s:4:"𝖷";s:1:"X";s:4:"𝖸";s:1:"Y";s:4:"𝖹";s:1:"Z";s:4:"𝖺";s:1:"a";s:4:"𝖻";s:1:"b";s:4:"𝖼";s:1:"c";s:4:"𝖽";s:1:"d";s:4:"𝖾";s:1:"e";s:4:"𝖿";s:1:"f";s:4:"𝗀";s:1:"g";s:4:"𝗁";s:1:"h";s:4:"𝗂";s:1:"i";s:4:"𝗃";s:1:"j";s:4:"𝗄";s:1:"k";s:4:"𝗅";s:1:"l";s:4:"𝗆";s:1:"m";s:4:"𝗇";s:1:"n";s:4:"𝗈";s:1:"o";s:4:"𝗉";s:1:"p";s:4:"𝗊";s:1:"q";s:4:"𝗋";s:1:"r";s:4:"𝗌";s:1:"s";s:4:"𝗍";s:1:"t";s:4:"𝗎";s:1:"u";s:4:"𝗏";s:1:"v";s:4:"𝗐";s:1:"w";s:4:"𝗑";s:1:"x";s:4:"𝗒";s:1:"y";s:4:"𝗓";s:1:"z";s:4:"𝗔";s:1:"A";s:4:"𝗕";s:1:"B";s:4:"𝗖";s:1:"C";s:4:"𝗗";s:1:"D";s:4:"𝗘";s:1:"E";s:4:"𝗙";s:1:"F";s:4:"𝗚";s:1:"G";s:4:"𝗛";s:1:"H";s:4:"𝗜";s:1:"I";s:4:"𝗝";s:1:"J";s:4:"𝗞";s:1:"K";s:4:"𝗟";s:1:"L";s:4:"𝗠";s:1:"M";s:4:"𝗡";s:1:"N";s:4:"𝗢";s:1:"O";s:4:"𝗣";s:1:"P";s:4:"𝗤";s:1:"Q";s:4:"𝗥";s:1:"R";s:4:"𝗦";s:1:"S";s:4:"𝗧";s:1:"T";s:4:"𝗨";s:1:"U";s:4:"𝗩";s:1:"V";s:4:"𝗪";s:1:"W";s:4:"𝗫";s:1:"X";s:4:"𝗬";s:1:"Y";s:4:"𝗭";s:1:"Z";s:4:"𝗮";s:1:"a";s:4:"𝗯";s:1:"b";s:4:"𝗰";s:1:"c";s:4:"𝗱";s:1:"d";s:4:"𝗲";s:1:"e";s:4:"𝗳";s:1:"f";s:4:"𝗴";s:1:"g";s:4:"𝗵";s:1:"h";s:4:"𝗶";s:1:"i";s:4:"𝗷";s:1:"j";s:4:"𝗸";s:1:"k";s:4:"𝗹";s:1:"l";s:4:"𝗺";s:1:"m";s:4:"𝗻";s:1:"n";s:4:"𝗼";s:1:"o";s:4:"𝗽";s:1:"p";s:4:"𝗾";s:1:"q";s:4:"𝗿";s:1:"r";s:4:"𝘀";s:1:"s";s:4:"𝘁";s:1:"t";s:4:"𝘂";s:1:"u";s:4:"𝘃";s:1:"v";s:4:"𝘄";s:1:"w";s:4:"𝘅";s:1:"x";s:4:"𝘆";s:1:"y";s:4:"𝘇";s:1:"z";s:4:"𝘈";s:1:"A";s:4:"𝘉";s:1:"B";s:4:"𝘊";s:1:"C";s:4:"𝘋";s:1:"D";s:4:"𝘌";s:1:"E";s:4:"𝘍";s:1:"F";s:4:"𝘎";s:1:"G";s:4:"𝘏";s:1:"H";s:4:"𝘐";s:1:"I";s:4:"𝘑";s:1:"J";s:4:"𝘒";s:1:"K";s:4:"𝘓";s:1:"L";s:4:"𝘔";s:1:"M";s:4:"𝘕";s:1:"N";s:4:"𝘖";s:1:"O";s:4:"𝘗";s:1:"P";s:4:"𝘘";s:1:"Q";s:4:"𝘙";s:1:"R";s:4:"𝘚";s:1:"S";s:4:"𝘛";s:1:"T";s:4:"𝘜";s:1:"U";s:4:"𝘝";s:1:"V";s:4:"𝘞";s:1:"W";s:4:"𝘟";s:1:"X";s:4:"𝘠";s:1:"Y";s:4:"𝘡";s:1:"Z";s:4:"𝘢";s:1:"a";s:4:"𝘣";s:1:"b";s:4:"𝘤";s:1:"c";s:4:"𝘥";s:1:"d";s:4:"𝘦";s:1:"e";s:4:"𝘧";s:1:"f";s:4:"𝘨";s:1:"g";s:4:"𝘩";s:1:"h";s:4:"𝘪";s:1:"i";s:4:"𝘫";s:1:"j";s:4:"𝘬";s:1:"k";s:4:"𝘭";s:1:"l";s:4:"𝘮";s:1:"m";s:4:"𝘯";s:1:"n";s:4:"𝘰";s:1:"o";s:4:"𝘱";s:1:"p";s:4:"𝘲";s:1:"q";s:4:"𝘳";s:1:"r";s:4:"𝘴";s:1:"s";s:4:"𝘵";s:1:"t";s:4:"𝘶";s:1:"u";s:4:"𝘷";s:1:"v";s:4:"𝘸";s:1:"w";s:4:"𝘹";s:1:"x";s:4:"𝘺";s:1:"y";s:4:"𝘻";s:1:"z";s:4:"𝘼";s:1:"A";s:4:"𝘽";s:1:"B";s:4:"𝘾";s:1:"C";s:4:"𝘿";s:1:"D";s:4:"𝙀";s:1:"E";s:4:"𝙁";s:1:"F";s:4:"𝙂";s:1:"G";s:4:"𝙃";s:1:"H";s:4:"𝙄";s:1:"I";s:4:"𝙅";s:1:"J";s:4:"𝙆";s:1:"K";s:4:"𝙇";s:1:"L";s:4:"𝙈";s:1:"M";s:4:"𝙉";s:1:"N";s:4:"𝙊";s:1:"O";s:4:"𝙋";s:1:"P";s:4:"𝙌";s:1:"Q";s:4:"𝙍";s:1:"R";s:4:"𝙎";s:1:"S";s:4:"𝙏";s:1:"T";s:4:"𝙐";s:1:"U";s:4:"𝙑";s:1:"V";s:4:"𝙒";s:1:"W";s:4:"𝙓";s:1:"X";s:4:"𝙔";s:1:"Y";s:4:"𝙕";s:1:"Z";s:4:"𝙖";s:1:"a";s:4:"𝙗";s:1:"b";s:4:"𝙘";s:1:"c";s:4:"𝙙";s:1:"d";s:4:"𝙚";s:1:"e";s:4:"𝙛";s:1:"f";s:4:"𝙜";s:1:"g";s:4:"𝙝";s:1:"h";s:4:"𝙞";s:1:"i";s:4:"𝙟";s:1:"j";s:4:"𝙠";s:1:"k";s:4:"𝙡";s:1:"l";s:4:"𝙢";s:1:"m";s:4:"𝙣";s:1:"n";s:4:"𝙤";s:1:"o";s:4:"𝙥";s:1:"p";s:4:"𝙦";s:1:"q";s:4:"𝙧";s:1:"r";s:4:"𝙨";s:1:"s";s:4:"𝙩";s:1:"t";s:4:"𝙪";s:1:"u";s:4:"𝙫";s:1:"v";s:4:"𝙬";s:1:"w";s:4:"𝙭";s:1:"x";s:4:"𝙮";s:1:"y";s:4:"𝙯";s:1:"z";s:4:"𝙰";s:1:"A";s:4:"𝙱";s:1:"B";s:4:"𝙲";s:1:"C";s:4:"𝙳";s:1:"D";s:4:"𝙴";s:1:"E";s:4:"𝙵";s:1:"F";s:4:"𝙶";s:1:"G";s:4:"𝙷";s:1:"H";s:4:"𝙸";s:1:"I";s:4:"𝙹";s:1:"J";s:4:"𝙺";s:1:"K";s:4:"𝙻";s:1:"L";s:4:"𝙼";s:1:"M";s:4:"𝙽";s:1:"N";s:4:"𝙾";s:1:"O";s:4:"𝙿";s:1:"P";s:4:"𝚀";s:1:"Q";s:4:"𝚁";s:1:"R";s:4:"𝚂";s:1:"S";s:4:"𝚃";s:1:"T";s:4:"𝚄";s:1:"U";s:4:"𝚅";s:1:"V";s:4:"𝚆";s:1:"W";s:4:"𝚇";s:1:"X";s:4:"𝚈";s:1:"Y";s:4:"𝚉";s:1:"Z";s:4:"𝚊";s:1:"a";s:4:"𝚋";s:1:"b";s:4:"𝚌";s:1:"c";s:4:"𝚍";s:1:"d";s:4:"𝚎";s:1:"e";s:4:"𝚏";s:1:"f";s:4:"𝚐";s:1:"g";s:4:"𝚑";s:1:"h";s:4:"𝚒";s:1:"i";s:4:"𝚓";s:1:"j";s:4:"𝚔";s:1:"k";s:4:"𝚕";s:1:"l";s:4:"𝚖";s:1:"m";s:4:"𝚗";s:1:"n";s:4:"𝚘";s:1:"o";s:4:"𝚙";s:1:"p";s:4:"𝚚";s:1:"q";s:4:"𝚛";s:1:"r";s:4:"𝚜";s:1:"s";s:4:"𝚝";s:1:"t";s:4:"𝚞";s:1:"u";s:4:"𝚟";s:1:"v";s:4:"𝚠";s:1:"w";s:4:"𝚡";s:1:"x";s:4:"𝚢";s:1:"y";s:4:"𝚣";s:1:"z";s:4:"𝚤";s:2:"ı";s:4:"𝚥";s:2:"ȷ";s:4:"𝚨";s:2:"Α";s:4:"𝚩";s:2:"Β";s:4:"𝚪";s:2:"Γ";s:4:"𝚫";s:2:"Δ";s:4:"𝚬";s:2:"Ε";s:4:"𝚭";s:2:"Ζ";s:4:"𝚮";s:2:"Η";s:4:"𝚯";s:2:"Θ";s:4:"𝚰";s:2:"Ι";s:4:"𝚱";s:2:"Κ";s:4:"𝚲";s:2:"Λ";s:4:"𝚳";s:2:"Μ";s:4:"𝚴";s:2:"Ν";s:4:"𝚵";s:2:"Ξ";s:4:"𝚶";s:2:"Ο";s:4:"𝚷";s:2:"Π";s:4:"𝚸";s:2:"Ρ";s:4:"𝚹";s:2:"Θ";s:4:"𝚺";s:2:"Σ";s:4:"𝚻";s:2:"Τ";s:4:"𝚼";s:2:"Υ";s:4:"𝚽";s:2:"Φ";s:4:"𝚾";s:2:"Χ";s:4:"𝚿";s:2:"Ψ";s:4:"𝛀";s:2:"Ω";s:4:"𝛁";s:3:"∇";s:4:"𝛂";s:2:"α";s:4:"𝛃";s:2:"β";s:4:"𝛄";s:2:"γ";s:4:"𝛅";s:2:"δ";s:4:"𝛆";s:2:"ε";s:4:"𝛇";s:2:"ζ";s:4:"𝛈";s:2:"η";s:4:"𝛉";s:2:"θ";s:4:"𝛊";s:2:"ι";s:4:"𝛋";s:2:"κ";s:4:"𝛌";s:2:"λ";s:4:"𝛍";s:2:"μ";s:4:"𝛎";s:2:"ν";s:4:"𝛏";s:2:"ξ";s:4:"𝛐";s:2:"ο";s:4:"𝛑";s:2:"π";s:4:"𝛒";s:2:"ρ";s:4:"𝛓";s:2:"ς";s:4:"𝛔";s:2:"σ";s:4:"𝛕";s:2:"τ";s:4:"𝛖";s:2:"υ";s:4:"𝛗";s:2:"φ";s:4:"𝛘";s:2:"χ";s:4:"𝛙";s:2:"ψ";s:4:"𝛚";s:2:"ω";s:4:"𝛛";s:3:"∂";s:4:"𝛜";s:2:"ε";s:4:"𝛝";s:2:"θ";s:4:"𝛞";s:2:"κ";s:4:"𝛟";s:2:"φ";s:4:"𝛠";s:2:"ρ";s:4:"𝛡";s:2:"π";s:4:"𝛢";s:2:"Α";s:4:"𝛣";s:2:"Β";s:4:"𝛤";s:2:"Γ";s:4:"𝛥";s:2:"Δ";s:4:"𝛦";s:2:"Ε";s:4:"𝛧";s:2:"Ζ";s:4:"𝛨";s:2:"Η";s:4:"𝛩";s:2:"Θ";s:4:"𝛪";s:2:"Ι";s:4:"𝛫";s:2:"Κ";s:4:"𝛬";s:2:"Λ";s:4:"𝛭";s:2:"Μ";s:4:"𝛮";s:2:"Ν";s:4:"𝛯";s:2:"Ξ";s:4:"𝛰";s:2:"Ο";s:4:"𝛱";s:2:"Π";s:4:"𝛲";s:2:"Ρ";s:4:"𝛳";s:2:"Θ";s:4:"𝛴";s:2:"Σ";s:4:"𝛵";s:2:"Τ";s:4:"𝛶";s:2:"Υ";s:4:"𝛷";s:2:"Φ";s:4:"𝛸";s:2:"Χ";s:4:"𝛹";s:2:"Ψ";s:4:"𝛺";s:2:"Ω";s:4:"𝛻";s:3:"∇";s:4:"𝛼";s:2:"α";s:4:"𝛽";s:2:"β";s:4:"𝛾";s:2:"γ";s:4:"𝛿";s:2:"δ";s:4:"𝜀";s:2:"ε";s:4:"𝜁";s:2:"ζ";s:4:"𝜂";s:2:"η";s:4:"𝜃";s:2:"θ";s:4:"𝜄";s:2:"ι";s:4:"𝜅";s:2:"κ";s:4:"𝜆";s:2:"λ";s:4:"𝜇";s:2:"μ";s:4:"𝜈";s:2:"ν";s:4:"𝜉";s:2:"ξ";s:4:"𝜊";s:2:"ο";s:4:"𝜋";s:2:"π";s:4:"𝜌";s:2:"ρ";s:4:"𝜍";s:2:"ς";s:4:"𝜎";s:2:"σ";s:4:"𝜏";s:2:"τ";s:4:"𝜐";s:2:"υ";s:4:"𝜑";s:2:"φ";s:4:"𝜒";s:2:"χ";s:4:"𝜓";s:2:"ψ";s:4:"𝜔";s:2:"ω";s:4:"𝜕";s:3:"∂";s:4:"𝜖";s:2:"ε";s:4:"𝜗";s:2:"θ";s:4:"𝜘";s:2:"κ";s:4:"𝜙";s:2:"φ";s:4:"𝜚";s:2:"ρ";s:4:"𝜛";s:2:"π";s:4:"𝜜";s:2:"Α";s:4:"𝜝";s:2:"Β";s:4:"𝜞";s:2:"Γ";s:4:"𝜟";s:2:"Δ";s:4:"𝜠";s:2:"Ε";s:4:"𝜡";s:2:"Ζ";s:4:"𝜢";s:2:"Η";s:4:"𝜣";s:2:"Θ";s:4:"𝜤";s:2:"Ι";s:4:"𝜥";s:2:"Κ";s:4:"𝜦";s:2:"Λ";s:4:"𝜧";s:2:"Μ";s:4:"𝜨";s:2:"Ν";s:4:"𝜩";s:2:"Ξ";s:4:"𝜪";s:2:"Ο";s:4:"𝜫";s:2:"Π";s:4:"𝜬";s:2:"Ρ";s:4:"𝜭";s:2:"Θ";s:4:"𝜮";s:2:"Σ";s:4:"𝜯";s:2:"Τ";s:4:"𝜰";s:2:"Υ";s:4:"𝜱";s:2:"Φ";s:4:"𝜲";s:2:"Χ";s:4:"𝜳";s:2:"Ψ";s:4:"𝜴";s:2:"Ω";s:4:"𝜵";s:3:"∇";s:4:"𝜶";s:2:"α";s:4:"𝜷";s:2:"β";s:4:"𝜸";s:2:"γ";s:4:"𝜹";s:2:"δ";s:4:"𝜺";s:2:"ε";s:4:"𝜻";s:2:"ζ";s:4:"𝜼";s:2:"η";s:4:"𝜽";s:2:"θ";s:4:"𝜾";s:2:"ι";s:4:"𝜿";s:2:"κ";s:4:"𝝀";s:2:"λ";s:4:"𝝁";s:2:"μ";s:4:"𝝂";s:2:"ν";s:4:"𝝃";s:2:"ξ";s:4:"𝝄";s:2:"ο";s:4:"𝝅";s:2:"π";s:4:"𝝆";s:2:"ρ";s:4:"𝝇";s:2:"ς";s:4:"𝝈";s:2:"σ";s:4:"𝝉";s:2:"τ";s:4:"𝝊";s:2:"υ";s:4:"𝝋";s:2:"φ";s:4:"𝝌";s:2:"χ";s:4:"𝝍";s:2:"ψ";s:4:"𝝎";s:2:"ω";s:4:"𝝏";s:3:"∂";s:4:"𝝐";s:2:"ε";s:4:"𝝑";s:2:"θ";s:4:"𝝒";s:2:"κ";s:4:"𝝓";s:2:"φ";s:4:"𝝔";s:2:"ρ";s:4:"𝝕";s:2:"π";s:4:"𝝖";s:2:"Α";s:4:"𝝗";s:2:"Β";s:4:"𝝘";s:2:"Γ";s:4:"𝝙";s:2:"Δ";s:4:"𝝚";s:2:"Ε";s:4:"𝝛";s:2:"Ζ";s:4:"𝝜";s:2:"Η";s:4:"𝝝";s:2:"Θ";s:4:"𝝞";s:2:"Ι";s:4:"𝝟";s:2:"Κ";s:4:"𝝠";s:2:"Λ";s:4:"𝝡";s:2:"Μ";s:4:"𝝢";s:2:"Ν";s:4:"𝝣";s:2:"Ξ";s:4:"𝝤";s:2:"Ο";s:4:"𝝥";s:2:"Π";s:4:"𝝦";s:2:"Ρ";s:4:"𝝧";s:2:"Θ";s:4:"𝝨";s:2:"Σ";s:4:"𝝩";s:2:"Τ";s:4:"𝝪";s:2:"Υ";s:4:"𝝫";s:2:"Φ";s:4:"𝝬";s:2:"Χ";s:4:"𝝭";s:2:"Ψ";s:4:"𝝮";s:2:"Ω";s:4:"𝝯";s:3:"∇";s:4:"𝝰";s:2:"α";s:4:"𝝱";s:2:"β";s:4:"𝝲";s:2:"γ";s:4:"𝝳";s:2:"δ";s:4:"𝝴";s:2:"ε";s:4:"𝝵";s:2:"ζ";s:4:"𝝶";s:2:"η";s:4:"𝝷";s:2:"θ";s:4:"𝝸";s:2:"ι";s:4:"𝝹";s:2:"κ";s:4:"𝝺";s:2:"λ";s:4:"𝝻";s:2:"μ";s:4:"𝝼";s:2:"ν";s:4:"𝝽";s:2:"ξ";s:4:"𝝾";s:2:"ο";s:4:"𝝿";s:2:"π";s:4:"𝞀";s:2:"ρ";s:4:"𝞁";s:2:"ς";s:4:"𝞂";s:2:"σ";s:4:"𝞃";s:2:"τ";s:4:"𝞄";s:2:"υ";s:4:"𝞅";s:2:"φ";s:4:"𝞆";s:2:"χ";s:4:"𝞇";s:2:"ψ";s:4:"𝞈";s:2:"ω";s:4:"𝞉";s:3:"∂";s:4:"𝞊";s:2:"ε";s:4:"𝞋";s:2:"θ";s:4:"𝞌";s:2:"κ";s:4:"𝞍";s:2:"φ";s:4:"𝞎";s:2:"ρ";s:4:"𝞏";s:2:"π";s:4:"𝞐";s:2:"Α";s:4:"𝞑";s:2:"Β";s:4:"𝞒";s:2:"Γ";s:4:"𝞓";s:2:"Δ";s:4:"𝞔";s:2:"Ε";s:4:"𝞕";s:2:"Ζ";s:4:"𝞖";s:2:"Η";s:4:"𝞗";s:2:"Θ";s:4:"𝞘";s:2:"Ι";s:4:"𝞙";s:2:"Κ";s:4:"𝞚";s:2:"Λ";s:4:"𝞛";s:2:"Μ";s:4:"𝞜";s:2:"Ν";s:4:"𝞝";s:2:"Ξ";s:4:"𝞞";s:2:"Ο";s:4:"𝞟";s:2:"Π";s:4:"𝞠";s:2:"Ρ";s:4:"𝞡";s:2:"Θ";s:4:"𝞢";s:2:"Σ";s:4:"𝞣";s:2:"Τ";s:4:"𝞤";s:2:"Υ";s:4:"𝞥";s:2:"Φ";s:4:"𝞦";s:2:"Χ";s:4:"𝞧";s:2:"Ψ";s:4:"𝞨";s:2:"Ω";s:4:"𝞩";s:3:"∇";s:4:"𝞪";s:2:"α";s:4:"𝞫";s:2:"β";s:4:"𝞬";s:2:"γ";s:4:"𝞭";s:2:"δ";s:4:"𝞮";s:2:"ε";s:4:"𝞯";s:2:"ζ";s:4:"𝞰";s:2:"η";s:4:"𝞱";s:2:"θ";s:4:"𝞲";s:2:"ι";s:4:"𝞳";s:2:"κ";s:4:"𝞴";s:2:"λ";s:4:"𝞵";s:2:"μ";s:4:"𝞶";s:2:"ν";s:4:"𝞷";s:2:"ξ";s:4:"𝞸";s:2:"ο";s:4:"𝞹";s:2:"π";s:4:"𝞺";s:2:"ρ";s:4:"𝞻";s:2:"ς";s:4:"𝞼";s:2:"σ";s:4:"𝞽";s:2:"τ";s:4:"𝞾";s:2:"υ";s:4:"𝞿";s:2:"φ";s:4:"𝟀";s:2:"χ";s:4:"𝟁";s:2:"ψ";s:4:"𝟂";s:2:"ω";s:4:"𝟃";s:3:"∂";s:4:"𝟄";s:2:"ε";s:4:"𝟅";s:2:"θ";s:4:"𝟆";s:2:"κ";s:4:"𝟇";s:2:"φ";s:4:"𝟈";s:2:"ρ";s:4:"𝟉";s:2:"π";s:4:"𝟊";s:2:"Ϝ";s:4:"𝟋";s:2:"ϝ";s:4:"𝟎";s:1:"0";s:4:"𝟏";s:1:"1";s:4:"𝟐";s:1:"2";s:4:"𝟑";s:1:"3";s:4:"𝟒";s:1:"4";s:4:"𝟓";s:1:"5";s:4:"𝟔";s:1:"6";s:4:"𝟕";s:1:"7";s:4:"𝟖";s:1:"8";s:4:"𝟗";s:1:"9";s:4:"𝟘";s:1:"0";s:4:"𝟙";s:1:"1";s:4:"𝟚";s:1:"2";s:4:"𝟛";s:1:"3";s:4:"𝟜";s:1:"4";s:4:"𝟝";s:1:"5";s:4:"𝟞";s:1:"6";s:4:"𝟟";s:1:"7";s:4:"𝟠";s:1:"8";s:4:"𝟡";s:1:"9";s:4:"𝟢";s:1:"0";s:4:"𝟣";s:1:"1";s:4:"𝟤";s:1:"2";s:4:"𝟥";s:1:"3";s:4:"𝟦";s:1:"4";s:4:"𝟧";s:1:"5";s:4:"𝟨";s:1:"6";s:4:"𝟩";s:1:"7";s:4:"𝟪";s:1:"8";s:4:"𝟫";s:1:"9";s:4:"𝟬";s:1:"0";s:4:"𝟭";s:1:"1";s:4:"𝟮";s:1:"2";s:4:"𝟯";s:1:"3";s:4:"𝟰";s:1:"4";s:4:"𝟱";s:1:"5";s:4:"𝟲";s:1:"6";s:4:"𝟳";s:1:"7";s:4:"𝟴";s:1:"8";s:4:"𝟵";s:1:"9";s:4:"𝟶";s:1:"0";s:4:"𝟷";s:1:"1";s:4:"𝟸";s:1:"2";s:4:"𝟹";s:1:"3";s:4:"𝟺";s:1:"4";s:4:"𝟻";s:1:"5";s:4:"𝟼";s:1:"6";s:4:"𝟽";s:1:"7";s:4:"𝟾";s:1:"8";s:4:"𝟿";s:1:"9";s:4:"丽";s:3:"丽";s:4:"丸";s:3:"丸";s:4:"乁";s:3:"乁";s:4:"𠄢";s:4:"𠄢";s:4:"你";s:3:"你";s:4:"侮";s:3:"侮";s:4:"侻";s:3:"侻";s:4:"倂";s:3:"倂";s:4:"偺";s:3:"偺";s:4:"備";s:3:"備";s:4:"僧";s:3:"僧";s:4:"像";s:3:"像";s:4:"㒞";s:3:"㒞";s:4:"𠘺";s:4:"𠘺";s:4:"免";s:3:"免";s:4:"兔";s:3:"兔";s:4:"兤";s:3:"兤";s:4:"具";s:3:"具";s:4:"𠔜";s:4:"𠔜";s:4:"㒹";s:3:"㒹";s:4:"內";s:3:"內";s:4:"再";s:3:"再";s:4:"𠕋";s:4:"𠕋";s:4:"冗";s:3:"冗";s:4:"冤";s:3:"冤";s:4:"仌";s:3:"仌";s:4:"冬";s:3:"冬";s:4:"况";s:3:"况";s:4:"𩇟";s:4:"𩇟";s:4:"凵";s:3:"凵";s:4:"刃";s:3:"刃";s:4:"㓟";s:3:"㓟";s:4:"刻";s:3:"刻";s:4:"剆";s:3:"剆";s:4:"割";s:3:"割";s:4:"剷";s:3:"剷";s:4:"㔕";s:3:"㔕";s:4:"勇";s:3:"勇";s:4:"勉";s:3:"勉";s:4:"勤";s:3:"勤";s:4:"勺";s:3:"勺";s:4:"包";s:3:"包";s:4:"匆";s:3:"匆";s:4:"北";s:3:"北";s:4:"卉";s:3:"卉";s:4:"卑";s:3:"卑";s:4:"博";s:3:"博";s:4:"即";s:3:"即";s:4:"卽";s:3:"卽";s:4:"卿";s:3:"卿";s:4:"卿";s:3:"卿";s:4:"卿";s:3:"卿";s:4:"𠨬";s:4:"𠨬";s:4:"灰";s:3:"灰";s:4:"及";s:3:"及";s:4:"叟";s:3:"叟";s:4:"𠭣";s:4:"𠭣";s:4:"叫";s:3:"叫";s:4:"叱";s:3:"叱";s:4:"吆";s:3:"吆";s:4:"咞";s:3:"咞";s:4:"吸";s:3:"吸";s:4:"呈";s:3:"呈";s:4:"周";s:3:"周";s:4:"咢";s:3:"咢";s:4:"哶";s:3:"哶";s:4:"唐";s:3:"唐";s:4:"啓";s:3:"啓";s:4:"啣";s:3:"啣";s:4:"善";s:3:"善";s:4:"善";s:3:"善";s:4:"喙";s:3:"喙";s:4:"喫";s:3:"喫";s:4:"喳";s:3:"喳";s:4:"嗂";s:3:"嗂";s:4:"圖";s:3:"圖";s:4:"嘆";s:3:"嘆";s:4:"圗";s:3:"圗";s:4:"噑";s:3:"噑";s:4:"噴";s:3:"噴";s:4:"切";s:3:"切";s:4:"壮";s:3:"壮";s:4:"城";s:3:"城";s:4:"埴";s:3:"埴";s:4:"堍";s:3:"堍";s:4:"型";s:3:"型";s:4:"堲";s:3:"堲";s:4:"報";s:3:"報";s:4:"墬";s:3:"墬";s:4:"𡓤";s:4:"𡓤";s:4:"売";s:3:"売";s:4:"壷";s:3:"壷";s:4:"夆";s:3:"夆";s:4:"多";s:3:"多";s:4:"夢";s:3:"夢";s:4:"奢";s:3:"奢";s:4:"𡚨";s:4:"𡚨";s:4:"𡛪";s:4:"𡛪";s:4:"姬";s:3:"姬";s:4:"娛";s:3:"娛";s:4:"娧";s:3:"娧";s:4:"姘";s:3:"姘";s:4:"婦";s:3:"婦";s:4:"㛮";s:3:"㛮";s:4:"㛼";s:3:"㛼";s:4:"嬈";s:3:"嬈";s:4:"嬾";s:3:"嬾";s:4:"嬾";s:3:"嬾";s:4:"𡧈";s:4:"𡧈";s:4:"寃";s:3:"寃";s:4:"寘";s:3:"寘";s:4:"寧";s:3:"寧";s:4:"寳";s:3:"寳";s:4:"𡬘";s:4:"𡬘";s:4:"寿";s:3:"寿";s:4:"将";s:3:"将";s:4:"当";s:3:"当";s:4:"尢";s:3:"尢";s:4:"㞁";s:3:"㞁";s:4:"屠";s:3:"屠";s:4:"屮";s:3:"屮";s:4:"峀";s:3:"峀";s:4:"岍";s:3:"岍";s:4:"𡷤";s:4:"𡷤";s:4:"嵃";s:3:"嵃";s:4:"𡷦";s:4:"𡷦";s:4:"嵮";s:3:"嵮";s:4:"嵫";s:3:"嵫";s:4:"嵼";s:3:"嵼";s:4:"巡";s:3:"巡";s:4:"巢";s:3:"巢";s:4:"㠯";s:3:"㠯";s:4:"巽";s:3:"巽";s:4:"帨";s:3:"帨";s:4:"帽";s:3:"帽";s:4:"幩";s:3:"幩";s:4:"㡢";s:3:"㡢";s:4:"𢆃";s:4:"𢆃";s:4:"㡼";s:3:"㡼";s:4:"庰";s:3:"庰";s:4:"庳";s:3:"庳";s:4:"庶";s:3:"庶";s:4:"廊";s:3:"廊";s:4:"𪎒";s:4:"𪎒";s:4:"廾";s:3:"廾";s:4:"𢌱";s:4:"𢌱";s:4:"𢌱";s:4:"𢌱";s:4:"舁";s:3:"舁";s:4:"弢";s:3:"弢";s:4:"弢";s:3:"弢";s:4:"㣇";s:3:"㣇";s:4:"𣊸";s:4:"𣊸";s:4:"𦇚";s:4:"𦇚";s:4:"形";s:3:"形";s:4:"彫";s:3:"彫";s:4:"㣣";s:3:"㣣";s:4:"徚";s:3:"徚";s:4:"忍";s:3:"忍";s:4:"志";s:3:"志";s:4:"忹";s:3:"忹";s:4:"悁";s:3:"悁";s:4:"㤺";s:3:"㤺";s:4:"㤜";s:3:"㤜";s:4:"悔";s:3:"悔";s:4:"𢛔";s:4:"𢛔";s:4:"惇";s:3:"惇";s:4:"慈";s:3:"慈";s:4:"慌";s:3:"慌";s:4:"慎";s:3:"慎";s:4:"慌";s:3:"慌";s:4:"慺";s:3:"慺";s:4:"憎";s:3:"憎";s:4:"憲";s:3:"憲";s:4:"憤";s:3:"憤";s:4:"憯";s:3:"憯";s:4:"懞";s:3:"懞";s:4:"懲";s:3:"懲";s:4:"懶";s:3:"懶";s:4:"成";s:3:"成";s:4:"戛";s:3:"戛";s:4:"扝";s:3:"扝";s:4:"抱";s:3:"抱";s:4:"拔";s:3:"拔";s:4:"捐";s:3:"捐";s:4:"𢬌";s:4:"𢬌";s:4:"挽";s:3:"挽";s:4:"拼";s:3:"拼";s:4:"捨";s:3:"捨";s:4:"掃";s:3:"掃";s:4:"揤";s:3:"揤";s:4:"𢯱";s:4:"𢯱";s:4:"搢";s:3:"搢";s:4:"揅";s:3:"揅";s:4:"掩";s:3:"掩";s:4:"㨮";s:3:"㨮";s:4:"摩";s:3:"摩";s:4:"摾";s:3:"摾";s:4:"撝";s:3:"撝";s:4:"摷";s:3:"摷";s:4:"㩬";s:3:"㩬";s:4:"敏";s:3:"敏";s:4:"敬";s:3:"敬";s:4:"𣀊";s:4:"𣀊";s:4:"旣";s:3:"旣";s:4:"書";s:3:"書";s:4:"晉";s:3:"晉";s:4:"㬙";s:3:"㬙";s:4:"暑";s:3:"暑";s:4:"㬈";s:3:"㬈";s:4:"㫤";s:3:"㫤";s:4:"冒";s:3:"冒";s:4:"冕";s:3:"冕";s:4:"最";s:3:"最";s:4:"暜";s:3:"暜";s:4:"肭";s:3:"肭";s:4:"䏙";s:3:"䏙";s:4:"朗";s:3:"朗";s:4:"望";s:3:"望";s:4:"朡";s:3:"朡";s:4:"杞";s:3:"杞";s:4:"杓";s:3:"杓";s:4:"𣏃";s:4:"𣏃";s:4:"㭉";s:3:"㭉";s:4:"柺";s:3:"柺";s:4:"枅";s:3:"枅";s:4:"桒";s:3:"桒";s:4:"梅";s:3:"梅";s:4:"𣑭";s:4:"𣑭";s:4:"梎";s:3:"梎";s:4:"栟";s:3:"栟";s:4:"椔";s:3:"椔";s:4:"㮝";s:3:"㮝";s:4:"楂";s:3:"楂";s:4:"榣";s:3:"榣";s:4:"槪";s:3:"槪";s:4:"檨";s:3:"檨";s:4:"𣚣";s:4:"𣚣";s:4:"櫛";s:3:"櫛";s:4:"㰘";s:3:"㰘";s:4:"次";s:3:"次";s:4:"𣢧";s:4:"𣢧";s:4:"歔";s:3:"歔";s:4:"㱎";s:3:"㱎";s:4:"歲";s:3:"歲";s:4:"殟";s:3:"殟";s:4:"殺";s:3:"殺";s:4:"殻";s:3:"殻";s:4:"𣪍";s:4:"𣪍";s:4:"𡴋";s:4:"𡴋";s:4:"𣫺";s:4:"𣫺";s:4:"汎";s:3:"汎";s:4:"𣲼";s:4:"𣲼";s:4:"沿";s:3:"沿";s:4:"泍";s:3:"泍";s:4:"汧";s:3:"汧";s:4:"洖";s:3:"洖";s:4:"派";s:3:"派";s:4:"海";s:3:"海";s:4:"流";s:3:"流";s:4:"浩";s:3:"浩";s:4:"浸";s:3:"浸";s:4:"涅";s:3:"涅";s:4:"𣴞";s:4:"𣴞";s:4:"洴";s:3:"洴";s:4:"港";s:3:"港";s:4:"湮";s:3:"湮";s:4:"㴳";s:3:"㴳";s:4:"滋";s:3:"滋";s:4:"滇";s:3:"滇";s:4:"𣻑";s:4:"𣻑";s:4:"淹";s:3:"淹";s:4:"潮";s:3:"潮";s:4:"𣽞";s:4:"𣽞";s:4:"𣾎";s:4:"𣾎";s:4:"濆";s:3:"濆";s:4:"瀹";s:3:"瀹";s:4:"瀞";s:3:"瀞";s:4:"瀛";s:3:"瀛";s:4:"㶖";s:3:"㶖";s:4:"灊";s:3:"灊";s:4:"災";s:3:"災";s:4:"灷";s:3:"灷";s:4:"炭";s:3:"炭";s:4:"𠔥";s:4:"𠔥";s:4:"煅";s:3:"煅";s:4:"𤉣";s:4:"𤉣";s:4:"熜";s:3:"熜";s:4:"𤎫";s:4:"𤎫";s:4:"爨";s:3:"爨";s:4:"爵";s:3:"爵";s:4:"牐";s:3:"牐";s:4:"𤘈";s:4:"𤘈";s:4:"犀";s:3:"犀";s:4:"犕";s:3:"犕";s:4:"𤜵";s:4:"𤜵";s:4:"𤠔";s:4:"𤠔";s:4:"獺";s:3:"獺";s:4:"王";s:3:"王";s:4:"㺬";s:3:"㺬";s:4:"玥";s:3:"玥";s:4:"㺸";s:3:"㺸";s:4:"㺸";s:3:"㺸";s:4:"瑇";s:3:"瑇";s:4:"瑜";s:3:"瑜";s:4:"瑱";s:3:"瑱";s:4:"璅";s:3:"璅";s:4:"瓊";s:3:"瓊";s:4:"㼛";s:3:"㼛";s:4:"甤";s:3:"甤";s:4:"𤰶";s:4:"𤰶";s:4:"甾";s:3:"甾";s:4:"𤲒";s:4:"𤲒";s:4:"異";s:3:"異";s:4:"𢆟";s:4:"𢆟";s:4:"瘐";s:3:"瘐";s:4:"𤾡";s:4:"𤾡";s:4:"𤾸";s:4:"𤾸";s:4:"𥁄";s:4:"𥁄";s:4:"㿼";s:3:"㿼";s:4:"䀈";s:3:"䀈";s:4:"直";s:3:"直";s:4:"𥃳";s:4:"𥃳";s:4:"𥃲";s:4:"𥃲";s:4:"𥄙";s:4:"𥄙";s:4:"𥄳";s:4:"𥄳";s:4:"眞";s:3:"眞";s:4:"真";s:3:"真";s:4:"真";s:3:"真";s:4:"睊";s:3:"睊";s:4:"䀹";s:3:"䀹";s:4:"瞋";s:3:"瞋";s:4:"䁆";s:3:"䁆";s:4:"䂖";s:3:"䂖";s:4:"𥐝";s:4:"𥐝";s:4:"硎";s:3:"硎";s:4:"碌";s:3:"碌";s:4:"磌";s:3:"磌";s:4:"䃣";s:3:"䃣";s:4:"𥘦";s:4:"𥘦";s:4:"祖";s:3:"祖";s:4:"𥚚";s:4:"𥚚";s:4:"𥛅";s:4:"𥛅";s:4:"福";s:3:"福";s:4:"秫";s:3:"秫";s:4:"䄯";s:3:"䄯";s:4:"穀";s:3:"穀";s:4:"穊";s:3:"穊";s:4:"穏";s:3:"穏";s:4:"𥥼";s:4:"𥥼";s:4:"𥪧";s:4:"𥪧";s:4:"𥪧";s:4:"𥪧";s:4:"竮";s:3:"竮";s:4:"䈂";s:3:"䈂";s:4:"𥮫";s:4:"𥮫";s:4:"篆";s:3:"篆";s:4:"築";s:3:"築";s:4:"䈧";s:3:"䈧";s:4:"𥲀";s:4:"𥲀";s:4:"糒";s:3:"糒";s:4:"䊠";s:3:"䊠";s:4:"糨";s:3:"糨";s:4:"糣";s:3:"糣";s:4:"紀";s:3:"紀";s:4:"𥾆";s:4:"𥾆";s:4:"絣";s:3:"絣";s:4:"䌁";s:3:"䌁";s:4:"緇";s:3:"緇";s:4:"縂";s:3:"縂";s:4:"繅";s:3:"繅";s:4:"䌴";s:3:"䌴";s:4:"𦈨";s:4:"𦈨";s:4:"𦉇";s:4:"𦉇";s:4:"䍙";s:3:"䍙";s:4:"𦋙";s:4:"𦋙";s:4:"罺";s:3:"罺";s:4:"𦌾";s:4:"𦌾";s:4:"羕";s:3:"羕";s:4:"翺";s:3:"翺";s:4:"者";s:3:"者";s:4:"𦓚";s:4:"𦓚";s:4:"𦔣";s:4:"𦔣";s:4:"聠";s:3:"聠";s:4:"𦖨";s:4:"𦖨";s:4:"聰";s:3:"聰";s:4:"𣍟";s:4:"𣍟";s:4:"䏕";s:3:"䏕";s:4:"育";s:3:"育";s:4:"脃";s:3:"脃";s:4:"䐋";s:3:"䐋";s:4:"脾";s:3:"脾";s:4:"媵";s:3:"媵";s:4:"𦞧";s:4:"𦞧";s:4:"𦞵";s:4:"𦞵";s:4:"𣎓";s:4:"𣎓";s:4:"𣎜";s:4:"𣎜";s:4:"舁";s:3:"舁";s:4:"舄";s:3:"舄";s:4:"辞";s:3:"辞";s:4:"䑫";s:3:"䑫";s:4:"芑";s:3:"芑";s:4:"芋";s:3:"芋";s:4:"芝";s:3:"芝";s:4:"劳";s:3:"劳";s:4:"花";s:3:"花";s:4:"芳";s:3:"芳";s:4:"芽";s:3:"芽";s:4:"苦";s:3:"苦";s:4:"𦬼";s:4:"𦬼";s:4:"若";s:3:"若";s:4:"茝";s:3:"茝";s:4:"荣";s:3:"荣";s:4:"莭";s:3:"莭";s:4:"茣";s:3:"茣";s:4:"莽";s:3:"莽";s:4:"菧";s:3:"菧";s:4:"著";s:3:"著";s:4:"荓";s:3:"荓";s:4:"菊";s:3:"菊";s:4:"菌";s:3:"菌";s:4:"菜";s:3:"菜";s:4:"𦰶";s:4:"𦰶";s:4:"𦵫";s:4:"𦵫";s:4:"𦳕";s:4:"𦳕";s:4:"䔫";s:3:"䔫";s:4:"蓱";s:3:"蓱";s:4:"蓳";s:3:"蓳";s:4:"蔖";s:3:"蔖";s:4:"𧏊";s:4:"𧏊";s:4:"蕤";s:3:"蕤";s:4:"𦼬";s:4:"𦼬";s:4:"䕝";s:3:"䕝";s:4:"䕡";s:3:"䕡";s:4:"𦾱";s:4:"𦾱";s:4:"𧃒";s:4:"𧃒";s:4:"䕫";s:3:"䕫";s:4:"虐";s:3:"虐";s:4:"虜";s:3:"虜";s:4:"虧";s:3:"虧";s:4:"虩";s:3:"虩";s:4:"蚩";s:3:"蚩";s:4:"蚈";s:3:"蚈";s:4:"蜎";s:3:"蜎";s:4:"蛢";s:3:"蛢";s:4:"蝹";s:3:"蝹";s:4:"蜨";s:3:"蜨";s:4:"蝫";s:3:"蝫";s:4:"螆";s:3:"螆";s:4:"䗗";s:3:"䗗";s:4:"蟡";s:3:"蟡";s:4:"蠁";s:3:"蠁";s:4:"䗹";s:3:"䗹";s:4:"衠";s:3:"衠";s:4:"衣";s:3:"衣";s:4:"𧙧";s:4:"𧙧";s:4:"裗";s:3:"裗";s:4:"裞";s:3:"裞";s:4:"䘵";s:3:"䘵";s:4:"裺";s:3:"裺";s:4:"㒻";s:3:"㒻";s:4:"𧢮";s:4:"𧢮";s:4:"𧥦";s:4:"𧥦";s:4:"䚾";s:3:"䚾";s:4:"䛇";s:3:"䛇";s:4:"誠";s:3:"誠";s:4:"諭";s:3:"諭";s:4:"變";s:3:"變";s:4:"豕";s:3:"豕";s:4:"𧲨";s:4:"𧲨";s:4:"貫";s:3:"貫";s:4:"賁";s:3:"賁";s:4:"贛";s:3:"贛";s:4:"起";s:3:"起";s:4:"𧼯";s:4:"𧼯";s:4:"𠠄";s:4:"𠠄";s:4:"跋";s:3:"跋";s:4:"趼";s:3:"趼";s:4:"跰";s:3:"跰";s:4:"𠣞";s:4:"𠣞";s:4:"軔";s:3:"軔";s:4:"輸";s:3:"輸";s:4:"𨗒";s:4:"𨗒";s:4:"𨗭";s:4:"𨗭";s:4:"邔";s:3:"邔";s:4:"郱";s:3:"郱";s:4:"鄑";s:3:"鄑";s:4:"𨜮";s:4:"𨜮";s:4:"鄛";s:3:"鄛";s:4:"鈸";s:3:"鈸";s:4:"鋗";s:3:"鋗";s:4:"鋘";s:3:"鋘";s:4:"鉼";s:3:"鉼";s:4:"鏹";s:3:"鏹";s:4:"鐕";s:3:"鐕";s:4:"𨯺";s:4:"𨯺";s:4:"開";s:3:"開";s:4:"䦕";s:3:"䦕";s:4:"閷";s:3:"閷";s:4:"𨵷";s:4:"𨵷";s:4:"䧦";s:3:"䧦";s:4:"雃";s:3:"雃";s:4:"嶲";s:3:"嶲";s:4:"霣";s:3:"霣";s:4:"𩅅";s:4:"𩅅";s:4:"𩈚";s:4:"𩈚";s:4:"䩮";s:3:"䩮";s:4:"䩶";s:3:"䩶";s:4:"韠";s:3:"韠";s:4:"𩐊";s:4:"𩐊";s:4:"䪲";s:3:"䪲";s:4:"𩒖";s:4:"𩒖";s:4:"頋";s:3:"頋";s:4:"頋";s:3:"頋";s:4:"頩";s:3:"頩";s:4:"𩖶";s:4:"𩖶";s:4:"飢";s:3:"飢";s:4:"䬳";s:3:"䬳";s:4:"餩";s:3:"餩";s:4:"馧";s:3:"馧";s:4:"駂";s:3:"駂";s:4:"駾";s:3:"駾";s:4:"䯎";s:3:"䯎";s:4:"𩬰";s:4:"𩬰";s:4:"鬒";s:3:"鬒";s:4:"鱀";s:3:"鱀";s:4:"鳽";s:3:"鳽";s:4:"䳎";s:3:"䳎";s:4:"䳭";s:3:"䳭";s:4:"鵧";s:3:"鵧";s:4:"𪃎";s:4:"𪃎";s:4:"䳸";s:3:"䳸";s:4:"𪄅";s:4:"𪄅";s:4:"𪈎";s:4:"𪈎";s:4:"𪊑";s:4:"𪊑";s:4:"麻";s:3:"麻";s:4:"䵖";s:3:"䵖";s:4:"黹";s:3:"黹";s:4:"黾";s:3:"黾";s:4:"鼅";s:3:"鼅";s:4:"鼏";s:3:"鼏";s:4:"鼖";s:3:"鼖";s:4:"鼻";s:3:"鼻";s:4:"𪘀";s:4:"𪘀";}' );
?>
diff --git a/includes/normal/UtfNormalDefines.php b/includes/normal/UtfNormalDefines.php
new file mode 100644
index 00000000..419f6f8c
--- /dev/null
+++ b/includes/normal/UtfNormalDefines.php
@@ -0,0 +1,53 @@
+<?php
+
+define( 'UNICODE_HANGUL_FIRST', 0xac00 );
+define( 'UNICODE_HANGUL_LAST', 0xd7a3 );
+
+define( 'UNICODE_HANGUL_LBASE', 0x1100 );
+define( 'UNICODE_HANGUL_VBASE', 0x1161 );
+define( 'UNICODE_HANGUL_TBASE', 0x11a7 );
+
+define( 'UNICODE_HANGUL_LCOUNT', 19 );
+define( 'UNICODE_HANGUL_VCOUNT', 21 );
+define( 'UNICODE_HANGUL_TCOUNT', 28 );
+define( 'UNICODE_HANGUL_NCOUNT', UNICODE_HANGUL_VCOUNT * UNICODE_HANGUL_TCOUNT );
+
+define( 'UNICODE_HANGUL_LEND', UNICODE_HANGUL_LBASE + UNICODE_HANGUL_LCOUNT - 1 );
+define( 'UNICODE_HANGUL_VEND', UNICODE_HANGUL_VBASE + UNICODE_HANGUL_VCOUNT - 1 );
+define( 'UNICODE_HANGUL_TEND', UNICODE_HANGUL_TBASE + UNICODE_HANGUL_TCOUNT - 1 );
+
+define( 'UNICODE_SURROGATE_FIRST', 0xd800 );
+define( 'UNICODE_SURROGATE_LAST', 0xdfff );
+define( 'UNICODE_MAX', 0x10ffff );
+define( 'UNICODE_REPLACEMENT', 0xfffd );
+
+
+define( 'UTF8_HANGUL_FIRST', "\xea\xb0\x80" /*codepointToUtf8( UNICODE_HANGUL_FIRST )*/ );
+define( 'UTF8_HANGUL_LAST', "\xed\x9e\xa3" /*codepointToUtf8( UNICODE_HANGUL_LAST )*/ );
+
+define( 'UTF8_HANGUL_LBASE', "\xe1\x84\x80" /*codepointToUtf8( UNICODE_HANGUL_LBASE )*/ );
+define( 'UTF8_HANGUL_VBASE', "\xe1\x85\xa1" /*codepointToUtf8( UNICODE_HANGUL_VBASE )*/ );
+define( 'UTF8_HANGUL_TBASE', "\xe1\x86\xa7" /*codepointToUtf8( UNICODE_HANGUL_TBASE )*/ );
+
+define( 'UTF8_HANGUL_LEND', "\xe1\x84\x92" /*codepointToUtf8( UNICODE_HANGUL_LEND )*/ );
+define( 'UTF8_HANGUL_VEND', "\xe1\x85\xb5" /*codepointToUtf8( UNICODE_HANGUL_VEND )*/ );
+define( 'UTF8_HANGUL_TEND', "\xe1\x87\x82" /*codepointToUtf8( UNICODE_HANGUL_TEND )*/ );
+
+define( 'UTF8_SURROGATE_FIRST', "\xed\xa0\x80" /*codepointToUtf8( UNICODE_SURROGATE_FIRST )*/ );
+define( 'UTF8_SURROGATE_LAST', "\xed\xbf\xbf" /*codepointToUtf8( UNICODE_SURROGATE_LAST )*/ );
+define( 'UTF8_MAX', "\xf4\x8f\xbf\xbf" /*codepointToUtf8( UNICODE_MAX )*/ );
+define( 'UTF8_REPLACEMENT', "\xef\xbf\xbd" /*codepointToUtf8( UNICODE_REPLACEMENT )*/ );
+#define( 'UTF8_REPLACEMENT', '!' );
+
+define( 'UTF8_OVERLONG_A', "\xc1\xbf" );
+define( 'UTF8_OVERLONG_B', "\xe0\x9f\xbf" );
+define( 'UTF8_OVERLONG_C', "\xf0\x8f\xbf\xbf" );
+
+# These two ranges are illegal
+define( 'UTF8_FDD0', "\xef\xb7\x90" /*codepointToUtf8( 0xfdd0 )*/ );
+define( 'UTF8_FDEF', "\xef\xb7\xaf" /*codepointToUtf8( 0xfdef )*/ );
+define( 'UTF8_FFFE', "\xef\xbf\xbe" /*codepointToUtf8( 0xfffe )*/ );
+define( 'UTF8_FFFF', "\xef\xbf\xbf" /*codepointToUtf8( 0xffff )*/ );
+
+define( 'UTF8_HEAD', false );
+define( 'UTF8_TAIL', true );
diff --git a/includes/normal/UtfNormalGenerate.php b/includes/normal/UtfNormalGenerate.php
index 83f3085e..a16e76a8 100644
--- a/includes/normal/UtfNormalGenerate.php
+++ b/includes/normal/UtfNormalGenerate.php
@@ -21,7 +21,7 @@
* This script generates UniNormalData.inc from the Unicode Character Database
* and supplementary files.
*
- * @addtogroup UtfNormal
+ * @ingroup UtfNormal
* @access private
*/
@@ -230,5 +230,3 @@ function callbackCompat( $matches ) {
}
return $matches[1];
}
-
-
diff --git a/includes/normal/UtfNormalTest.php b/includes/normal/UtfNormalTest.php
index 556cf11a..ee1da4d0 100644
--- a/includes/normal/UtfNormalTest.php
+++ b/includes/normal/UtfNormalTest.php
@@ -20,7 +20,7 @@
/**
* Implements the conformance test at:
* http://www.unicode.org/Public/UNIDATA/NormalizationTest.txt
- * @addtogroup UtfNormal
+ * @ingroup UtfNormal
*/
/** */
@@ -245,5 +245,3 @@ function testInvariant( &$u, $char, $desc, $reportFailure = false ) {
}
return $result;
}
-
-
diff --git a/includes/normal/UtfNormalUtil.php b/includes/normal/UtfNormalUtil.php
index e68c6ec5..d772a203 100644
--- a/includes/normal/UtfNormalUtil.php
+++ b/includes/normal/UtfNormalUtil.php
@@ -21,11 +21,12 @@
* Some of these functions are adapted from places in MediaWiki.
* Should probably merge them for consistency.
*
- * @addtogroup UtfNormal
+ * @ingroup UtfNormal
* @public
*/
/** */
+require_once dirname(__FILE__).'/UtfNormalDefines.php';
/**
* Return UTF-8 sequence for a given Unicode code point.
@@ -138,5 +139,3 @@ function escapeSingleString( $string ) {
'\'' => '\\\''
));
}
-
-
diff --git a/includes/parser/CoreParserFunctions.php b/includes/parser/CoreParserFunctions.php
new file mode 100644
index 00000000..d9072e93
--- /dev/null
+++ b/includes/parser/CoreParserFunctions.php
@@ -0,0 +1,385 @@
+<?php
+
+/**
+ * Various core parser functions, registered in Parser::firstCallInit()
+ * @ingroup Parser
+ */
+class CoreParserFunctions {
+ static function register( $parser ) {
+ global $wgAllowDisplayTitle, $wgAllowSlowParserFunctions;
+
+ # 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:...}})
+
+ $parser->setFunctionHook( 'int', array( __CLASS__, 'intFunction' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'ns', array( __CLASS__, 'ns' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'urlencode', array( __CLASS__, 'urlencode' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'lcfirst', array( __CLASS__, 'lcfirst' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'ucfirst', array( __CLASS__, 'ucfirst' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'lc', array( __CLASS__, 'lc' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'uc', array( __CLASS__, 'uc' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'localurl', array( __CLASS__, 'localurl' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'localurle', array( __CLASS__, 'localurle' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'fullurl', array( __CLASS__, 'fullurl' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'fullurle', array( __CLASS__, 'fullurle' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'formatnum', array( __CLASS__, 'formatnum' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'grammar', array( __CLASS__, 'grammar' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'plural', array( __CLASS__, 'plural' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'numberofpages', array( __CLASS__, 'numberofpages' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'numberofusers', array( __CLASS__, 'numberofusers' ), SFH_NO_HASH );
+ $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( 'numberofedits', array( __CLASS__, 'numberofedits' ), 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 );
+ $parser->setFunctionHook( 'anchorencode', array( __CLASS__, 'anchorencode' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'special', array( __CLASS__, 'special' ) );
+ $parser->setFunctionHook( 'defaultsort', array( __CLASS__, 'defaultsort' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'filepath', array( __CLASS__, 'filepath' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'pagesincategory', array( __CLASS__, 'pagesincategory' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'pagesize', array( __CLASS__, 'pagesize' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'tag', array( __CLASS__, 'tagObj' ), SFH_OBJECT_ARGS );
+
+ if ( $wgAllowDisplayTitle ) {
+ $parser->setFunctionHook( 'displaytitle', array( __CLASS__, 'displaytitle' ), SFH_NO_HASH );
+ }
+ if ( $wgAllowSlowParserFunctions ) {
+ $parser->setFunctionHook( 'pagesinnamespace', array( __CLASS__, 'pagesinnamespace' ), SFH_NO_HASH );
+ }
+ }
+
+ static function intFunction( $parser, $part1 = '' /*, ... */ ) {
+ if ( strval( $part1 ) !== '' ) {
+ $args = array_slice( func_get_args(), 2 );
+ return wfMsgReal( $part1, $args, true );
+ } else {
+ return array( 'found' => false );
+ }
+ }
+
+ static function ns( $parser, $part1 = '' ) {
+ global $wgContLang;
+ $found = false;
+ if ( intval( $part1 ) || $part1 == "0" ) {
+ $text = $wgContLang->getNsText( intval( $part1 ) );
+ $found = true;
+ } else {
+ $param = str_replace( ' ', '_', strtolower( $part1 ) );
+ $index = MWNamespace::getCanonicalIndex( strtolower( $param ) );
+ if ( !is_null( $index ) ) {
+ $text = $wgContLang->getNsText( $index );
+ $found = true;
+ }
+ }
+ if ( $found ) {
+ return $text;
+ } else {
+ return array( 'found' => false );
+ }
+ }
+
+ static function urlencode( $parser, $s = '' ) {
+ return urlencode( $s );
+ }
+
+ static function lcfirst( $parser, $s = '' ) {
+ global $wgContLang;
+ return $wgContLang->lcfirst( $s );
+ }
+
+ static function ucfirst( $parser, $s = '' ) {
+ global $wgContLang;
+ return $wgContLang->ucfirst( $s );
+ }
+
+ static function lc( $parser, $s = '' ) {
+ global $wgContLang;
+ if ( is_callable( array( $parser, 'markerSkipCallback' ) ) ) {
+ return $parser->markerSkipCallback( $s, array( $wgContLang, 'lc' ) );
+ } else {
+ return $wgContLang->lc( $s );
+ }
+ }
+
+ static function uc( $parser, $s = '' ) {
+ global $wgContLang;
+ if ( is_callable( array( $parser, 'markerSkipCallback' ) ) ) {
+ return $parser->markerSkipCallback( $s, array( $wgContLang, 'uc' ) );
+ } else {
+ return $wgContLang->uc( $s );
+ }
+ }
+
+ static function localurl( $parser, $s = '', $arg = null ) { return self::urlFunction( 'getLocalURL', $s, $arg ); }
+ static function localurle( $parser, $s = '', $arg = null ) { return self::urlFunction( 'escapeLocalURL', $s, $arg ); }
+ static function fullurl( $parser, $s = '', $arg = null ) { return self::urlFunction( 'getFullURL', $s, $arg ); }
+ static function fullurle( $parser, $s = '', $arg = null ) { return self::urlFunction( 'escapeFullURL', $s, $arg ); }
+
+ static function urlFunction( $func, $s = '', $arg = null ) {
+ $title = Title::newFromText( $s );
+ # Due to order of execution of a lot of bits, the values might be encoded
+ # before arriving here; if that's true, then the title can't be created
+ # and the variable will fail. If we can't get a decent title from the first
+ # 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 ) ) {
+ $text = $title->$func( $arg );
+ } else {
+ $text = $title->$func();
+ }
+ return $text;
+ } else {
+ return array( 'found' => false );
+ }
+ }
+
+ static function formatNum( $parser, $num = '', $raw = null) {
+ if ( self::israw( $raw ) ) {
+ return $parser->getFunctionLang()->parseFormattedNumber( $num );
+ } else {
+ return $parser->getFunctionLang()->formatNum( $num );
+ }
+ }
+
+ static function grammar( $parser, $case = '', $word = '' ) {
+ return $parser->getFunctionLang()->convertGrammar( $word, $case );
+ }
+
+ static function plural( $parser, $text = '') {
+ $forms = array_slice( func_get_args(), 2);
+ $text = $parser->getFunctionLang()->parseFormattedNumber( $text );
+ return $parser->getFunctionLang()->convertPlural( $text, $forms );
+ }
+
+ /**
+ * Override the title of the page when viewed, provided we've been given a
+ * title which will normalise to the canonical title
+ *
+ * @param Parser $parser Parent parser
+ * @param string $text Desired title text
+ * @return string
+ */
+ static function displaytitle( $parser, $text = '' ) {
+ $text = trim( Sanitizer::decodeCharReferences( $text ) );
+ $title = Title::newFromText( $text );
+ if( $title instanceof Title && $title->getFragment() == '' && $title->equals( $parser->mTitle ) )
+ $parser->mOutput->setDisplayTitle( $text );
+ return '';
+ }
+
+ static function isRaw( $param ) {
+ static $mwRaw;
+ if ( !$mwRaw ) {
+ $mwRaw =& MagicWord::get( 'rawsuffix' );
+ }
+ if ( is_null( $param ) ) {
+ return false;
+ } else {
+ return $mwRaw->match( $param );
+ }
+ }
+
+ static function formatRaw( $num, $raw ) {
+ if( self::isRaw( $raw ) ) {
+ return $num;
+ } else {
+ global $wgContLang;
+ return $wgContLang->formatNum( $num );
+ }
+ }
+ static function numberofpages( $parser, $raw = null ) {
+ return self::formatRaw( SiteStats::pages(), $raw );
+ }
+ static function numberofusers( $parser, $raw = null ) {
+ return self::formatRaw( SiteStats::users(), $raw );
+ }
+ static function numberofarticles( $parser, $raw = null ) {
+ return self::formatRaw( SiteStats::articles(), $raw );
+ }
+ static function numberoffiles( $parser, $raw = null ) {
+ return self::formatRaw( SiteStats::images(), $raw );
+ }
+ static function numberofadmins( $parser, $raw = null ) {
+ return self::formatRaw( SiteStats::admins(), $raw );
+ }
+ static function numberofedits( $parser, $raw = null ) {
+ return self::formatRaw( SiteStats::edits(), $raw );
+ }
+ static function pagesinnamespace( $parser, $namespace = 0, $raw = null ) {
+ return self::formatRaw( SiteStats::pagesInNs( intval( $namespace ) ), $raw );
+ }
+
+ /**
+ * Return the number of pages in the given category, or 0 if it's nonexis-
+ * tent. This is an expensive parser function and can't be called too many
+ * times per page.
+ */
+ static function pagesincategory( $parser, $name = '', $raw = null ) {
+ static $cache = array();
+ $category = Category::newFromName( $name );
+
+ if( !is_object( $category ) ) {
+ $cache[$name] = 0;
+ return self::formatRaw( 0, $raw );
+ }
+
+ # Normalize name for cache
+ $name = $category->getName();
+
+ $count = 0;
+ if( isset( $cache[$name] ) ) {
+ $count = $cache[$name];
+ } elseif( $parser->incrementExpensiveFunctionCount() ) {
+ $count = $cache[$name] = (int)$category->getPageCount();
+ }
+ return self::formatRaw( $count, $raw );
+ }
+
+ /**
+ * Return the size of the given page, or 0 if it's nonexistent. This is an
+ * expensive parser function and can't be called too many times per page.
+ *
+ * @FIXME This doesn't work correctly on preview for getting the size of
+ * the current page.
+ * @FIXME Title::getLength() documentation claims that it adds things to
+ * the link cache, so the local cache here should be unnecessary, but in
+ * fact calling getLength() repeatedly for the same $page does seem to
+ * run one query for each call?
+ */
+ static function pagesize( $parser, $page = '', $raw = null ) {
+ static $cache = array();
+ $title = Title::newFromText($page);
+
+ if( !is_object( $title ) ) {
+ $cache[$page] = 0;
+ return self::formatRaw( 0, $raw );
+ }
+
+ # Normalize name for cache
+ $page = $title->getPrefixedText();
+
+ $length = 0;
+ if( isset( $cache[$page] ) ) {
+ $length = $cache[$page];
+ } elseif( $parser->incrementExpensiveFunctionCount() ) {
+ $length = $cache[$page] = $title->getLength();
+
+ // Register dependency in templatelinks
+ $id = $title->getArticleId();
+ $revid = Revision::newFromTitle($title);
+ $parser->mOutput->addTemplate($title, $id, $revid);
+ }
+ return self::formatRaw( $length, $raw );
+ }
+
+ static function language( $parser, $arg = '' ) {
+ global $wgContLang;
+ $lang = $wgContLang->getLanguageName( strtolower( $arg ) );
+ return $lang != '' ? $lang : $arg;
+ }
+
+ static function pad( $string = '', $length = 0, $char = 0, $direction = STR_PAD_RIGHT ) {
+ $length = min( max( $length, 0 ), 500 );
+ $char = substr( $char, 0, 1 );
+ return ( $string !== '' && (int)$length > 0 && strlen( trim( (string)$char ) ) > 0 )
+ ? str_pad( $string, $length, (string)$char, $direction )
+ : $string;
+ }
+
+ static function padleft( $parser, $string = '', $length = 0, $char = 0 ) {
+ return self::pad( $string, $length, $char, STR_PAD_LEFT );
+ }
+
+ static function padright( $parser, $string = '', $length = 0, $char = 0 ) {
+ return self::pad( $string, $length, $char );
+ }
+
+ static function anchorencode( $parser, $text ) {
+ $a = urlencode( $text );
+ $a = strtr( $a, array( '%' => '.', '+' => '_' ) );
+ # leave colons alone, however
+ $a = str_replace( '.3A', ':', $a );
+ return $a;
+ }
+
+ static function special( $parser, $text ) {
+ $title = SpecialPage::getTitleForAlias( $text );
+ if ( $title ) {
+ return $title->getPrefixedText();
+ } else {
+ return wfMsgForContent( 'nosuchspecialpage' );
+ }
+ }
+
+ public static function defaultsort( $parser, $text ) {
+ $text = trim( $text );
+ if( strlen( $text ) > 0 )
+ $parser->setDefaultSort( $text );
+ return '';
+ }
+
+ public static function filepath( $parser, $name='', $option='' ) {
+ $file = wfFindFile( $name );
+ if( $file ) {
+ $url = $file->getFullUrl();
+ if( $option == 'nowiki' ) {
+ return "<nowiki>$url</nowiki>";
+ }
+ return $url;
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Parser function to extension tag adaptor
+ */
+ public static function tagObj( $parser, $frame, $args ) {
+ $xpath = false;
+ if ( !count( $args ) ) {
+ return '';
+ }
+ $tagName = strtolower( trim( $frame->expand( array_shift( $args ) ) ) );
+
+ if ( count( $args ) ) {
+ $inner = $frame->expand( array_shift( $args ) );
+ } else {
+ $inner = null;
+ }
+
+ $stripList = $parser->getStripList();
+ if ( !in_array( $tagName, $stripList ) ) {
+ return '<span class="error">' .
+ wfMsg( 'unknown_extension_tag', $tagName ) .
+ '</span>';
+ }
+
+ $attributes = array();
+ foreach ( $args as $arg ) {
+ $bits = $arg->splitArg();
+ if ( strval( $bits['index'] ) === '' ) {
+ $name = $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] : '';
+ }
+ $attributes[$name] = $value;
+ }
+ }
+
+ $params = array(
+ 'name' => $tagName,
+ 'inner' => $inner,
+ 'attributes' => $attributes,
+ 'close' => "</$tagName>",
+ );
+ return $parser->extensionSubstitution( $params, $frame );
+ }
+}
diff --git a/includes/parser/DateFormatter.php b/includes/parser/DateFormatter.php
new file mode 100644
index 00000000..9ef11d5e
--- /dev/null
+++ b/includes/parser/DateFormatter.php
@@ -0,0 +1,283 @@
+<?php
+
+/**
+ * Date formatter, recognises dates in plain text and formats them accoding to user preferences.
+ * @todo preferences, OutputPage
+ * @ingroup Parser
+ */
+class DateFormatter
+{
+ var $mSource, $mTarget;
+ var $monthNames = '', $rxDM, $rxMD, $rxDMY, $rxYDM, $rxMDY, $rxYMD;
+
+ var $regexes, $pDays, $pMonths, $pYears;
+ var $rules, $xMonths, $preferences;
+
+ const ALL = -1;
+ const NONE = 0;
+ const MDY = 1;
+ const DMY = 2;
+ const YMD = 3;
+ const ISO1 = 4;
+ const LASTPREF = 4;
+ const ISO2 = 5;
+ const YDM = 6;
+ const DM = 7;
+ const MD = 8;
+ const LAST = 8;
+
+ /**
+ * @todo document
+ */
+ function DateFormatter() {
+ global $wgContLang;
+
+ $this->monthNames = $this->getMonthRegex();
+ for ( $i=1; $i<=12; $i++ ) {
+ $this->xMonths[$wgContLang->lc( $wgContLang->getMonthName( $i ) )] = $i;
+ $this->xMonths[$wgContLang->lc( $wgContLang->getMonthAbbreviation( $i ) )] = $i;
+ }
+
+ $this->regexTrail = '(?![a-z])/iu';
+
+ # Partial regular expressions
+ $this->prxDM = '\[\[(\d{1,2})[ _](' . $this->monthNames . ')]]';
+ $this->prxMD = '\[\[(' . $this->monthNames . ')[ _](\d{1,2})]]';
+ $this->prxY = '\[\[(\d{1,4}([ _]BC|))]]';
+ $this->prxISO1 = '\[\[(-?\d{4})]]-\[\[(\d{2})-(\d{2})]]';
+ $this->prxISO2 = '\[\[(-?\d{4})-(\d{2})-(\d{2})]]';
+
+ # Real regular expressions
+ $this->regexes[self::DMY] = "/{$this->prxDM} *,? *{$this->prxY}{$this->regexTrail}";
+ $this->regexes[self::YDM] = "/{$this->prxY} *,? *{$this->prxDM}{$this->regexTrail}";
+ $this->regexes[self::MDY] = "/{$this->prxMD} *,? *{$this->prxY}{$this->regexTrail}";
+ $this->regexes[self::YMD] = "/{$this->prxY} *,? *{$this->prxMD}{$this->regexTrail}";
+ $this->regexes[self::DM] = "/{$this->prxDM}{$this->regexTrail}";
+ $this->regexes[self::MD] = "/{$this->prxMD}{$this->regexTrail}";
+ $this->regexes[self::ISO1] = "/{$this->prxISO1}{$this->regexTrail}";
+ $this->regexes[self::ISO2] = "/{$this->prxISO2}{$this->regexTrail}";
+
+ # Extraction keys
+ # See the comments in replace() for the meaning of the letters
+ $this->keys[self::DMY] = 'jFY';
+ $this->keys[self::YDM] = 'Y jF';
+ $this->keys[self::MDY] = 'FjY';
+ $this->keys[self::YMD] = 'Y Fj';
+ $this->keys[self::DM] = 'jF';
+ $this->keys[self::MD] = 'Fj';
+ $this->keys[self::ISO1] = 'ymd'; # y means ISO year
+ $this->keys[self::ISO2] = 'ymd';
+
+ # Target date formats
+ $this->targets[self::DMY] = '[[F j|j F]] [[Y]]';
+ $this->targets[self::YDM] = '[[Y]], [[F j|j F]]';
+ $this->targets[self::MDY] = '[[F j]], [[Y]]';
+ $this->targets[self::YMD] = '[[Y]] [[F j]]';
+ $this->targets[self::DM] = '[[F j|j F]]';
+ $this->targets[self::MD] = '[[F j]]';
+ $this->targets[self::ISO1] = '[[Y|y]]-[[F j|m-d]]';
+ $this->targets[self::ISO2] = '[[y-m-d]]';
+
+ # Rules
+ # pref source target
+ $this->rules[self::DMY][self::MD] = self::DM;
+ $this->rules[self::ALL][self::MD] = self::MD;
+ $this->rules[self::MDY][self::DM] = self::MD;
+ $this->rules[self::ALL][self::DM] = self::DM;
+ $this->rules[self::NONE][self::ISO2] = self::ISO1;
+
+ $this->preferences = array(
+ 'default' => self::NONE,
+ 'dmy' => self::DMY,
+ 'mdy' => self::MDY,
+ 'ymd' => self::YMD,
+ 'ISO 8601' => self::ISO1,
+ );
+ }
+
+ /**
+ * @static
+ */
+ function &getInstance() {
+ global $wgMemc;
+ static $dateFormatter = false;
+ if ( !$dateFormatter ) {
+ $dateFormatter = $wgMemc->get( wfMemcKey( 'dateformatter' ) );
+ if ( !$dateFormatter ) {
+ $dateFormatter = new DateFormatter;
+ $wgMemc->set( wfMemcKey( 'dateformatter' ), $dateFormatter, 3600 );
+ }
+ }
+ return $dateFormatter;
+ }
+
+ /**
+ * @param string $preference User preference
+ * @param string $text Text to reformat
+ */
+ function reformat( $preference, $text ) {
+ if ( isset( $this->preferences[$preference] ) ) {
+ $preference = $this->preferences[$preference];
+ } else {
+ $preference = self::NONE;
+ }
+ for ( $i=1; $i<=self::LAST; $i++ ) {
+ $this->mSource = $i;
+ if ( isset ( $this->rules[$preference][$i] ) ) {
+ # Specific rules
+ $this->mTarget = $this->rules[$preference][$i];
+ } elseif ( isset ( $this->rules[self::ALL][$i] ) ) {
+ # General rules
+ $this->mTarget = $this->rules[self::ALL][$i];
+ } elseif ( $preference ) {
+ # User preference
+ $this->mTarget = $preference;
+ } else {
+ # Default
+ $this->mTarget = $i;
+ }
+ $text = preg_replace_callback( $this->regexes[$i], array( &$this, 'replace' ), $text );
+ }
+ return $text;
+ }
+
+ /**
+ * @param $matches
+ */
+ function replace( $matches ) {
+ # Extract information from $matches
+ $bits = array();
+ $key = $this->keys[$this->mSource];
+ for ( $p=0; $p < strlen($key); $p++ ) {
+ if ( $key{$p} != ' ' ) {
+ $bits[$key{$p}] = $matches[$p+1];
+ }
+ }
+
+ $format = $this->targets[$this->mTarget];
+
+ # Construct new date
+ $text = '';
+ $fail = false;
+
+ for ( $p=0; $p < strlen( $format ); $p++ ) {
+ $char = $format{$p};
+ switch ( $char ) {
+ case 'd': # ISO day of month
+ if ( !isset($bits['d']) ) {
+ $text .= sprintf( '%02d', $bits['j'] );
+ } else {
+ $text .= $bits['d'];
+ }
+ break;
+ case 'm': # ISO month
+ if ( !isset($bits['m']) ) {
+ $m = $this->makeIsoMonth( $bits['F'] );
+ if ( !$m || $m == '00' ) {
+ $fail = true;
+ } else {
+ $text .= $m;
+ }
+ } else {
+ $text .= $bits['m'];
+ }
+ break;
+ case 'y': # ISO year
+ if ( !isset( $bits['y'] ) ) {
+ $text .= $this->makeIsoYear( $bits['Y'] );
+ } else {
+ $text .= $bits['y'];
+ }
+ break;
+ case 'j': # ordinary day of month
+ if ( !isset($bits['j']) ) {
+ $text .= intval( $bits['d'] );
+ } else {
+ $text .= $bits['j'];
+ }
+ break;
+ case 'F': # long month
+ if ( !isset( $bits['F'] ) ) {
+ $m = intval($bits['m']);
+ if ( $m > 12 || $m < 1 ) {
+ $fail = true;
+ } else {
+ global $wgContLang;
+ $text .= $wgContLang->getMonthName( $m );
+ }
+ } else {
+ $text .= ucfirst( $bits['F'] );
+ }
+ break;
+ case 'Y': # ordinary (optional BC) year
+ if ( !isset( $bits['Y'] ) ) {
+ $text .= $this->makeNormalYear( $bits['y'] );
+ } else {
+ $text .= $bits['Y'];
+ }
+ break;
+ default:
+ $text .= $char;
+ }
+ }
+ if ( $fail ) {
+ $text = $matches[0];
+ }
+ return $text;
+ }
+
+ /**
+ * @todo document
+ */
+ function getMonthRegex() {
+ global $wgContLang;
+ $names = array();
+ for( $i = 1; $i <= 12; $i++ ) {
+ $names[] = $wgContLang->getMonthName( $i );
+ $names[] = $wgContLang->getMonthAbbreviation( $i );
+ }
+ return implode( '|', $names );
+ }
+
+ /**
+ * Makes an ISO month, e.g. 02, from a month name
+ * @param $monthName String: month name
+ * @return string ISO month name
+ */
+ function makeIsoMonth( $monthName ) {
+ global $wgContLang;
+
+ $n = $this->xMonths[$wgContLang->lc( $monthName )];
+ return sprintf( '%02d', $n );
+ }
+
+ /**
+ * @todo document
+ * @param $year String: Year name
+ * @return string ISO year name
+ */
+ function makeIsoYear( $year ) {
+ # Assumes the year is in a nice format, as enforced by the regex
+ if ( substr( $year, -2 ) == 'BC' ) {
+ $num = intval(substr( $year, 0, -3 )) - 1;
+ # PHP bug note: sprintf( "%04d", -1 ) fails poorly
+ $text = sprintf( '-%04d', $num );
+
+ } else {
+ $text = sprintf( '%04d', $year );
+ }
+ return $text;
+ }
+
+ /**
+ * @todo document
+ */
+ function makeNormalYear( $iso ) {
+ if ( $iso{0} == '-' ) {
+ $text = (intval( substr( $iso, 1 ) ) + 1) . ' BC';
+ } else {
+ $text = intval( $iso );
+ }
+ return $text;
+ }
+}
diff --git a/includes/parser/Parser.php b/includes/parser/Parser.php
new file mode 100644
index 00000000..e41aa1ac
--- /dev/null
+++ b/includes/parser/Parser.php
@@ -0,0 +1,5002 @@
+<?php
+/**
+ * @defgroup Parser Parser
+ *
+ * @file
+ * @ingroup Parser
+ * File for Parser and related classes
+ */
+
+
+/**
+ * PHP Parser - Processes wiki markup (which uses a more user-friendly
+ * syntax, such as "[[link]]" for making links), and provides a one-way
+ * transformation of that wiki markup it into XHTML output / markup
+ * (which in turn the browser understands, and can display).
+ *
+ * <pre>
+ * There are five main entry points into the Parser class:
+ * parse()
+ * produces HTML output
+ * preSaveTransform().
+ * produces altered wiki markup.
+ * preprocess()
+ * removes HTML comments and expands templates
+ * cleanSig()
+ * Cleans a signature before saving it to preferences
+ * extractSections()
+ * Extracts sections from an article for section editing
+ *
+ * Globals used:
+ * objects: $wgLang, $wgContLang
+ *
+ * NOT $wgArticle, $wgUser or $wgTitle. Keep them away!
+ *
+ * settings:
+ * $wgUseTex*, $wgUseDynamicDates*, $wgInterwikiMagic*,
+ * $wgNamespacesWithSubpages, $wgAllowExternalImages*,
+ * $wgLocaltimezone, $wgAllowSpecialInclusion*,
+ * $wgMaxArticleSize*
+ *
+ * * only within ParserOptions
+ * </pre>
+ *
+ * @ingroup Parser
+ */
+class 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::setFunctionHook
+ # Also available as global constants from Defines.php
+ const SFH_NO_HASH = 1;
+ const SFH_OBJECT_ARGS = 2;
+
+ # 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';
+
+ // 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;
+
+ // Flags for preprocessToDom
+ const PTD_FOR_INCLUSION = 1;
+
+ // Allowed values for $this->mOutputType
+ // Parameter to startExternalParse().
+ const OT_HTML = 1;
+ const OT_WIKI = 2;
+ const OT_PREPROCESS = 3;
+ const OT_MSG = 3;
+
+ // Marker Suffix needs to be accessible staticly.
+ const MARKER_SUFFIX = "-QINU\x7f";
+
+ /**#@+
+ * @private
+ */
+ # Persistent:
+ var $mTagHooks, $mTransparentTagHooks, $mFunctionHooks, $mFunctionSynonyms, $mVariables,
+ $mImageParams, $mImageParamsMagicArray, $mStripList, $mMarkerIndex, $mPreprocessor,
+ $mExtLinkBracketedRegex, $mDefaultStripList, $mVarCache, $mConf;
+
+
+ # Cleared with clearState():
+ var $mOutput, $mAutonumber, $mDTopen, $mStripState;
+ var $mIncludeCount, $mArgStack, $mLastSection, $mInPre;
+ var $mInterwikiLinkHolders, $mLinkHolders;
+ var $mIncludeSizes, $mPPNodeCount, $mDefaultSort;
+ var $mTplExpandCache; // empty-frame expansion cache
+ var $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores;
+ var $mExpensiveFunctionCount; // number of expensive parser function calls
+
+ # 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->mConf = $conf;
+ $this->mTagHooks = array();
+ $this->mTransparentTagHooks = array();
+ $this->mFunctionHooks = array();
+ $this->mFunctionSynonyms = array( 0 => array(), 1 => array() );
+ $this->mDefaultStripList = $this->mStripList = array( 'nowiki', 'gallery' );
+ $this->mExtLinkBracketedRegex = '/\[(\b(' . wfUrlProtocols() . ')'.
+ '[^][<>"\\x00-\\x20\\x7F]+) *([^\]\\x0a\\x0d]*?)\]/S';
+ $this->mVarCache = array();
+ if ( isset( $conf['preprocessorClass'] ) ) {
+ $this->mPreprocessorClass = $conf['preprocessorClass'];
+ } elseif ( extension_loaded( 'dom' ) ) {
+ $this->mPreprocessorClass = 'Preprocessor_DOM';
+ } else {
+ $this->mPreprocessorClass = 'Preprocessor_Hash';
+ }
+ $this->mMarkerIndex = 0;
+ $this->mFirstCall = true;
+ }
+
+ /**
+ * Do various kinds of initialisation on the first call of the parser
+ */
+ function firstCallInit() {
+ if ( !$this->mFirstCall ) {
+ return;
+ }
+ $this->mFirstCall = false;
+
+ wfProfileIn( __METHOD__ );
+
+ $this->setHook( 'pre', array( $this, 'renderPreTag' ) );
+ CoreParserFunctions::register( $this );
+ $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 = 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->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.
+ *
+ * Must not consist of all title characters, or else it will change
+ * the behaviour of <nowiki> in a link.
+ */
+ #$this->mUniqPrefix = "\x07UNIQ" . Parser::getRandomString();
+ # Changed to \x7f to allow XML double-parsing -- TS
+ $this->mUniqPrefix = "\x7fUNIQ" . Parser::getRandomString();
+
+
+ # Clear these on every parse, bug 4549
+ $this->mTplExpandCache = $this->mTplRedirCache = $this->mTplDomCache = array();
+
+ $this->mShowToc = true;
+ $this->mForceTocPosition = false;
+ $this->mIncludeSizes = array(
+ 'post-expand' => 0,
+ 'arg' => 0,
+ );
+ $this->mPPNodeCount = 0;
+ $this->mDefaultSort = false;
+ $this->mHeadings = array();
+ $this->mDoubleUnderscores = array();
+ $this->mExpensiveFunctionCount = 0;
+
+ # Fix cloning
+ if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
+ $this->mPreprocessor = null;
+ }
+
+ 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,
+ 'pre' => $ot == self::OT_PREPROCESS,
+ );
+ }
+
+ /**
+ * Set the context title
+ */
+ function setTitle( $t ) {
+ if ( !$t || $t instanceof FakeTitle ) {
+ $t = Title::newFromText( 'NO TITLE' );
+ }
+ if ( strval( $t->getFragment() ) !== '' ) {
+ # Strip the fragment to avoid various odd effects
+ $this->mTitle = clone $t;
+ $this->mTitle->setFragment( '' );
+ } else {
+ $this->mTitle = $t;
+ }
+ }
+
+ /**
+ * Accessor for mUniqPrefix.
+ *
+ * @public
+ */
+ function uniqPrefix() {
+ if( !isset( $this->mUniqPrefix ) ) {
+ // @fixme this is probably *horribly wrong*
+ // LanguageConverter seems to want $wgParser's uniqPrefix, however
+ // if this is called for a parser cache hit, the parser may not
+ // have ever been initialized in the first place.
+ // Not really sure what the heck is supposed to be going on here.
+ return '';
+ //throw new MWException( "Accessing uninitialized mUniqPrefix" );
+ }
+ 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->setTitle( $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 ) );
+ # No more strip!
+ 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;',
+ '/&nbsp;(!\s*important)/' => ' \\1', #Beware of CSS magic word !important, bug #11874.
+ );
+ $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 = Parser::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix );
+
+ foreach( $matches as $marker => $data ) {
+ list( $element, $content, $params, $tag ) = $data;
+ $tagName = strtolower( $element );
+ if( isset( $this->mTransparentTagHooks[$tagName] ) ) {
+ $output = call_user_func_array( $this->mTransparentTagHooks[$tagName],
+ array( $content, $params, $this ) );
+ } else {
+ $output = $tag;
+ }
+ $this->mStripState->general->setPair( $marker, $output );
+ }
+ $text = $this->mStripState->unstripGeneral( $text );
+
+ $text = Sanitizer::normalizeCharReferences( $text );
+
+ if (($wgUseTidy and $this->mOptions->mTidy) or $wgAlwaysUseTidy) {
+ $text = Parser::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 );
+ }
+ global $wgExpensiveParserFunctionLimit;
+ if ( $this->mExpensiveFunctionCount > $wgExpensiveParserFunctionLimit ) {
+ $this->limitationWarn( 'expensive-parserfunction', $this->mExpensiveFunctionCount, $wgExpensiveParserFunctionLimit );
+ }
+
+ wfRunHooks( 'ParserAfterTidy', array( &$this, &$text ) );
+
+ # Information on include size limits, for the benefit of users who try to skirt them
+ if ( $this->mOptions->getEnableLimitReport() ) {
+ global $wgExpensiveParserFunctionLimit;
+ $max = $this->mOptions->getMaxIncludeSize();
+ $PFreport = "Expensive parser function count: {$this->mExpensiveFunctionCount}/$wgExpensiveParserFunctionLimit\n";
+ $limitReport =
+ "NewPP limit report\n" .
+ "Preprocessor node count: {$this->mPPNodeCount}/{$this->mOptions->mMaxPPNodeCount}\n" .
+ "Post-expand include size: {$this->mIncludeSizes['post-expand']}/$max bytes\n" .
+ "Template argument size: {$this->mIncludeSizes['arg']}/$max bytes\n".
+ $PFreport;
+ wfRunHooks( 'ParserLimitReport', array( $this, &$limitReport ) );
+ $text .= "\n<!-- \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 ) );
+ 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->setTitle( $title );
+ if( $revid !== null ) {
+ $this->mRevisionId = $revid;
+ }
+ wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
+ wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
+ $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;
+
+ $target = $this->mOptions->getTargetLanguage();
+ if ( $target !== null ) {
+ return $target;
+ } else {
+ return $this->mOptions->getInterfaceMessage() ? $wgLang : $wgContLang;
+ }
+ }
+
+ /**
+ * Get a preprocessor object
+ */
+ function getPreprocessor() {
+ if ( !isset( $this->mPreprocessor ) ) {
+ $class = $this->mPreprocessorClass;
+ $this->mPreprocessor = new $class( $this );
+ }
+ return $this->mPreprocessor;
+ }
+
+ /**
+ * Replaces all occurrences of HTML-style comments and the given tags
+ * in the text with a random marker and returns the 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++) . self::MARKER_SUFFIX;
+ $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;
+ }
+
+ /**
+ * Get a list of strippable XML-like elements
+ */
+ function getStripList() {
+ global $wgRawHtml;
+ $elements = $this->mStripList;
+ if( $wgRawHtml ) {
+ $elements[] = 'html';
+ }
+ if( $this->mOptions->getUseTeX() ) {
+ $elements[] = 'math';
+ }
+ return $elements;
+ }
+
+ /**
+ * @deprecated use replaceVariables
+ */
+ function strip( $text, $state, $stripcomments = false , $dontstrip = array () ) {
+ 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 ) {
+ $rnd = "{$this->mUniqPrefix}-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
+ $this->mMarkerIndex++;
+ $this->mStripState->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 = Parser::internalTidy( $wrappedtext );
+ } else {
+ $correctedtext = Parser::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, $wgDebugTidy;
+ $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 );
+ }
+ if ( $wgDebugTidy && $tidy->getStatus() > 0 ) {
+ $cleansource .= "<!--\nTidy reports:\n" .
+ str_replace( '-->', '--&gt;', $tidy->errorBuffer ) .
+ "\n-->";
+ }
+
+ 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 == self::OT_HTML.
+ *
+ * @private
+ */
+ function internalParse( $text ) {
+ $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 ;
+ }
+
+ $text = $this->replaceVariables( $text );
+ $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ), false, array_keys( $this->mTransparentTagHooks ) );
+ 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->doDoubleUnderscore( $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', $num );
+ $text = '<a href="' .
+ $titleObj->escapeLocalUrl() .
+ "\" 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>", $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 = Parser::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 = Parser::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( 'Parser', '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"; }
+
+ $useLinkPrefixExtension = $wgContLang->linkPrefixExtension();
+ $e2 = null;
+ if ( $useLinkPrefixExtension ) {
+ # 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' );
+ }
+
+ if( is_null( $this->mTitle ) ) {
+ wfProfileOut( $fname );
+ wfProfileOut( $fname.'-setup' );
+ 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 ;
+ wfProfileOut( "$fname-misc" );
+ 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
+ return MWNamespace::hasSubpages( $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{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.
+ */
+ if ( wfRunHooks( 'ParserGetVariableValueVarCache', array( &$this, &$this->mVarCache ) ) ) {
+ if ( isset( $this->mVarCache[$index] ) ) {
+ return $this->mVarCache[$index];
+ }
+ }
+
+ $ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() );
+ 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 $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'm', $ts ) );
+ case 'currentmonthname':
+ return $this->mVarCache[$index] = $wgContLang->getMonthName( gmdate( 'n', $ts ) );
+ case 'currentmonthnamegen':
+ return $this->mVarCache[$index] = $wgContLang->getMonthNameGen( gmdate( 'n', $ts ) );
+ case 'currentmonthabbrev':
+ return $this->mVarCache[$index] = $wgContLang->getMonthAbbreviation( gmdate( 'n', $ts ) );
+ case 'currentday':
+ return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'j', $ts ) );
+ case 'currentday2':
+ return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'd', $ts ) );
+ case 'localmonth':
+ return $this->mVarCache[$index] = $wgContLang->formatNum( $localMonth );
+ case 'localmonthname':
+ return $this->mVarCache[$index] = $wgContLang->getMonthName( $localMonthName );
+ case 'localmonthnamegen':
+ return $this->mVarCache[$index] = $wgContLang->getMonthNameGen( $localMonthName );
+ case 'localmonthabbrev':
+ return $this->mVarCache[$index] = $wgContLang->getMonthAbbreviation( $localMonthName );
+ case 'localday':
+ return $this->mVarCache[$index] = $wgContLang->formatNum( $localDay );
+ case 'localday2':
+ return $this->mVarCache[$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':
+ // Let the edit saving system know we should parse the page
+ // *after* a revision ID has been assigned.
+ $this->mOutput->setFlag( 'vary-revision' );
+ wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision...\n" );
+ return $this->mRevisionId;
+ case 'revisionday':
+ // Let the edit saving system know we should parse the page
+ // *after* a revision ID has been assigned. This is for null edits.
+ $this->mOutput->setFlag( 'vary-revision' );
+ wfDebug( __METHOD__ . ": {{REVISIONDAY}} used, setting vary-revision...\n" );
+ return intval( substr( $this->getRevisionTimestamp(), 6, 2 ) );
+ case 'revisionday2':
+ // Let the edit saving system know we should parse the page
+ // *after* a revision ID has been assigned. This is for null edits.
+ $this->mOutput->setFlag( 'vary-revision' );
+ wfDebug( __METHOD__ . ": {{REVISIONDAY2}} used, setting vary-revision...\n" );
+ return substr( $this->getRevisionTimestamp(), 6, 2 );
+ case 'revisionmonth':
+ // Let the edit saving system know we should parse the page
+ // *after* a revision ID has been assigned. This is for null edits.
+ $this->mOutput->setFlag( 'vary-revision' );
+ wfDebug( __METHOD__ . ": {{REVISIONMONTH}} used, setting vary-revision...\n" );
+ return intval( substr( $this->getRevisionTimestamp(), 4, 2 ) );
+ case 'revisionyear':
+ // Let the edit saving system know we should parse the page
+ // *after* a revision ID has been assigned. This is for null edits.
+ $this->mOutput->setFlag( 'vary-revision' );
+ wfDebug( __METHOD__ . ": {{REVISIONYEAR}} used, setting vary-revision...\n" );
+ return substr( $this->getRevisionTimestamp(), 0, 4 );
+ case 'revisiontimestamp':
+ // Let the edit saving system know we should parse the page
+ // *after* a revision ID has been assigned. This is for null edits.
+ $this->mOutput->setFlag( 'vary-revision' );
+ wfDebug( __METHOD__ . ": {{REVISIONTIMESTAMP}} used, setting vary-revision...\n" );
+ 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 $this->mVarCache[$index] = $wgContLang->getWeekdayName( gmdate( 'w', $ts ) + 1 );
+ case 'currentyear':
+ return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'Y', $ts ), true );
+ case 'currenttime':
+ return $this->mVarCache[$index] = $wgContLang->time( wfTimestamp( TS_MW, $ts ), false, false );
+ case 'currenthour':
+ return $this->mVarCache[$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 $this->mVarCache[$index] = $wgContLang->formatNum( (int)gmdate( 'W', $ts ) );
+ case 'currentdow':
+ return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'w', $ts ) );
+ case 'localdayname':
+ return $this->mVarCache[$index] = $wgContLang->getWeekdayName( $localDayOfWeek + 1 );
+ case 'localyear':
+ return $this->mVarCache[$index] = $wgContLang->formatNum( $localYear, true );
+ case 'localtime':
+ return $this->mVarCache[$index] = $wgContLang->time( $localTimestamp, false, false );
+ case 'localhour':
+ return $this->mVarCache[$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 $this->mVarCache[$index] = $wgContLang->formatNum( (int)$localWeek );
+ case 'localdow':
+ return $this->mVarCache[$index] = $wgContLang->formatNum( $localDayOfWeek );
+ case 'numberofarticles':
+ return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::articles() );
+ case 'numberoffiles':
+ return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::images() );
+ case 'numberofusers':
+ return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::users() );
+ case 'numberofpages':
+ return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::pages() );
+ case 'numberofadmins':
+ return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::admins() );
+ case 'numberofedits':
+ return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::edits() );
+ case 'currenttimestamp':
+ return $this->mVarCache[$index] = wfTimestamp( TS_MW, $ts );
+ case 'localtimestamp':
+ return $this->mVarCache[$index] = $localTimestamp;
+ case 'currentversion':
+ return $this->mVarCache[$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, &$this->mVarCache, &$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 = new MagicWordArray( $variableIDs );
+ wfProfileOut( $fname );
+ }
+
+ /**
+ * Preprocess some wikitext and return the document tree.
+ * This is the ghost of replace_variables().
+ *
+ * @param string $text The text to parse
+ * @param integer flags Bitwise combination of:
+ * self::PTD_FOR_INCLUSION Handle <noinclude>/<includeonly> as if the text is being
+ * included. Default is to assume a direct page view.
+ *
+ * The generated DOM tree must depend only on the input text and the flags.
+ * The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of bug 4899.
+ *
+ * Any flag added to the $flags parameter here, or any other parameter liable to cause a
+ * change in the DOM tree for a given text, must be passed through the section identifier
+ * in the section edit link and thus back to extractSections().
+ *
+ * The output of this function is currently only cached in process memory, but a persistent
+ * cache may be implemented at a later date which takes further advantage of these strict
+ * dependency requirements.
+ *
+ * @private
+ */
+ function preprocessToDom ( $text, $flags = 0 ) {
+ $dom = $this->getPreprocessor()->preprocessToObj( $text, $flags );
+ return $dom;
+ }
+
+ /*
+ * Return a three-element array: leading whitespace, string contents, trailing whitespace
+ */
+ public static function splitWhitespace( $s ) {
+ $ltrimmed = ltrim( $s );
+ $w1 = substr( $s, 0, strlen( $s ) - strlen( $ltrimmed ) );
+ $trimmed = rtrim( $ltrimmed );
+ $diff = strlen( $ltrimmed ) - strlen( $trimmed );
+ if ( $diff > 0 ) {
+ $w2 = substr( $ltrimmed, -$diff );
+ } else {
+ $w2 = '';
+ }
+ return array( $w1, $trimmed, $w2 );
+ }
+
+ /**
+ * 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_PREPROCESS: templates but not extension tags
+ * self::OT_HTML: all templates and extension tags
+ *
+ * @param string $tex The text to transform
+ * @param PPFrame $frame Object describing the arguments passed to the template.
+ * Arguments may also be provided as an associative array, as was the usual case before MW1.12.
+ * Providing arguments this way may be useful for extensions wishing to perform variable replacement explicitly.
+ * @param bool $argsOnly Only do argument (triple-brace) expansion, not double-brace expansion
+ * @private
+ */
+ function replaceVariables( $text, $frame = false, $argsOnly = false ) {
+ # Prevent too big inclusions
+ if( strlen( $text ) > $this->mOptions->getMaxIncludeSize() ) {
+ return $text;
+ }
+
+ $fname = __METHOD__;
+ wfProfileIn( $fname );
+
+ if ( $frame === false ) {
+ $frame = $this->getPreprocessor()->newFrame();
+ } elseif ( !( $frame instanceof PPFrame ) ) {
+ wfDebug( __METHOD__." called using plain parameters instead of a PPFrame instance. Creating custom frame.\n" );
+ $frame = $this->getPreprocessor()->newCustomFrame($frame);
+ }
+
+ $dom = $this->preprocessToDom( $text );
+ $flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
+ $text = $frame->expand( $dom, $flags );
+
+ 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;
+ }
+
+ /**
+ * Warn the user when a parser limitation is reached
+ * Will warn at most once the user per limitation type
+ *
+ * @param string $limitationType, should be one of:
+ * 'expensive-parserfunction' (corresponding messages: 'expensive-parserfunction-warning', 'expensive-parserfunction-category')
+ * 'post-expand-template-argument' (corresponding messages: 'post-expand-template-argument-warning', 'post-expand-template-argument-category')
+ * 'post-expand-template-inclusion' (corresponding messages: 'post-expand-template-inclusion-warning', 'post-expand-template-inclusion-category')
+ * @params int $current, $max When an explicit limit has been
+ * exceeded, provide the values (optional)
+ */
+ 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);
+ $this->mOutput->addWarning( $warning );
+ $cat = Title::makeTitleSafe( NS_CATEGORY, wfMsgForContent( $limitationType . '-category' ) );
+ if ( $cat ) {
+ $this->mOutput->addCategory( $cat->getDBkey(), $this->getDefaultSort() );
+ }
+ }
+
+ /**
+ * 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['title']: the title, i.e. the part before the |
+ * $piece['parts']: the parameter array
+ * $piece['lineStart']: whether the brace was at the start of a line
+ * @param PPFrame The current frame, contains template arguments
+ * @return string the text of the template
+ * @private
+ */
+ function braceSubstitution( $piece, $frame ) {
+ global $wgContLang, $wgLang, $wgAllowDisplayTitle, $wgNonincludableNamespaces;
+ $fname = __METHOD__;
+ wfProfileIn( $fname );
+ wfProfileIn( __METHOD__.'-setup' );
+
+ # Flags
+ $found = false; # $text has been filled
+ $nowiki = false; # wiki markup in $text should be escaped
+ $isHTML = false; # $text is HTML, armour it against wikitext transformation
+ $forceRawInterwiki = false; # Force interwiki transclusion to be done in raw mode not rendered
+ $isChildObj = false; # $text is a DOM node needing expansion in a child frame
+ $isLocalObj = false; # $text is a DOM node needing expansion in the current frame
+
+ # Title object, where $text came from
+ $title = NULL;
+
+ # $part1 is the bit before the first |, and must contain only title characters.
+ # Various prefixes will be stripped from it later.
+ $titleWithSpaces = $frame->expand( $piece['title'] );
+ $part1 = trim( $titleWithSpaces );
+ $titleText = false;
+
+ # Original title text preserved for various purposes
+ $originalTitle = $part1;
+
+ # $args is a list of argument nodes, starting from index 0, not including $part1
+ $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 = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
+ $isLocalObj = true;
+ $found = true;
+ }
+ }
+
+ # Variables
+ if ( !$found && $args->getLength() == 0 ) {
+ $id = $this->mVariables->matchStartToEnd( $part1 );
+ if ( $id !== false ) {
+ $text = $this->getVariableValue( $id );
+ if (MagicWord::getCacheTTL($id)>-1)
+ $this->mOutput->mContainsOldMagic = true;
+ $found = 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' );
+
+ # 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 ) {
+ list( $callback, $flags ) = $this->mFunctionHooks[$function];
+ $initialArgs = array( &$this );
+ $funcArgs = array( trim( substr( $part1, $colonPos + 1 ) ) );
+ if ( $flags & SFH_OBJECT_ARGS ) {
+ # Add a frame parameter, and pass the arguments as an array
+ $allArgs = $initialArgs;
+ $allArgs[] = $frame;
+ for ( $i = 0; $i < $args->getLength(); $i++ ) {
+ $funcArgs[] = $args->item( $i );
+ }
+ $allArgs[] = $funcArgs;
+ } else {
+ # Convert arguments to plain text
+ for ( $i = 0; $i < $args->getLength(); $i++ ) {
+ $funcArgs[] = trim( $frame->expand( $args->item( $i ) ) );
+ }
+ $allArgs = array_merge( $initialArgs, $funcArgs );
+ }
+
+ # Workaround for PHP bug 35229 and similar
+ if ( !is_callable( $callback ) ) {
+ throw new MWException( "Tag hook for $name is not callable\n" );
+ }
+ $result = call_user_func_array( $callback, $allArgs );
+ $found = true;
+ $noparse = true;
+ $preprocessFlags = 0;
+
+ if ( is_array( $result ) ) {
+ if ( isset( $result[0] ) ) {
+ $text = $result[0];
+ unset( $result[0] );
+ }
+
+ // Extract flags into the local scope
+ // This allows callers to set flags such as nowiki, found, etc.
+ extract( $result );
+ } else {
+ $text = $result;
+ }
+ if ( !$noparse ) {
+ $text = $this->preprocessToDom( $text, $preprocessFlags );
+ $isChildObj = true;
+ }
+ }
+ }
+ wfProfileOut( __METHOD__ . '-pfunc' );
+ }
+
+ # Finish mangling title and then check for loops.
+ # Set $title to a Title object and $titleText to the PDBK
+ if ( !$found ) {
+ $ns = NS_TEMPLATE;
+ # Split the title into page and subpage
+ $subpage = '';
+ $part1 = $this->maybeDoSubpageLink( $part1, $subpage );
+ if ($subpage !== '') {
+ $ns = $this->mTitle->getNamespace();
+ }
+ $title = Title::newFromText( $part1, $ns );
+ if ( $title ) {
+ $titleText = $title->getPrefixedText();
+ # Check for language variants if the template is not found
+ if($wgContLang->hasVariants() && $title->getArticleID() == 0){
+ $wgContLang->findVariantLink($part1, $title);
+ }
+ # Do infinite loop check
+ if ( !$frame->loopCheck( $title ) ) {
+ $found = true;
+ $text = "<span class=\"error\">Template loop detected: [[$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>";
+ }
+ }
+ }
+
+ # Load from database
+ if ( !$found && $title ) {
+ wfProfileIn( __METHOD__ . '-loadtpl' );
+ if ( !$title->isExternal() ) {
+ if ( $title->getNamespace() == NS_SPECIAL && $this->mOptions->getAllowSpecialInclusion() && $this->ot['html'] ) {
+ $text = SpecialPage::capturePath( $title );
+ if ( is_string( $text ) ) {
+ $found = 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( $text, $title ) = $this->getTemplateDom( $title );
+ if ( $text !== false ) {
+ $found = true;
+ $isChildObj = 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;
+ } else {
+ $text = $this->interwikiTransclude( $title, 'raw' );
+ // Preprocess it like a template
+ $text = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
+ $isChildObj = true;
+ }
+ $found = true;
+ }
+ wfProfileOut( __METHOD__ . '-loadtpl' );
+ }
+
+ # If we haven't found text to substitute by now, we're done
+ # Recover the source wikitext and return it
+ if ( !$found ) {
+ $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
+ wfProfileOut( $fname );
+ return array( 'object' => $text );
+ }
+
+ # Expand DOM-style return values in a child frame
+ if ( $isChildObj ) {
+ # Clean up argument array
+ $newFrame = $frame->newChild( $args, $title );
+
+ if ( $nowiki ) {
+ $text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG );
+ } elseif ( $titleText !== false && $newFrame->isEmpty() ) {
+ # Expansion is eligible for the empty-frame cache
+ if ( isset( $this->mTplExpandCache[$titleText] ) ) {
+ $text = $this->mTplExpandCache[$titleText];
+ } else {
+ $text = $newFrame->expand( $text );
+ $this->mTplExpandCache[$titleText] = $text;
+ }
+ } else {
+ # Uncached expansion
+ $text = $newFrame->expand( $text );
+ }
+ }
+ if ( $isLocalObj && $nowiki ) {
+ $text = $frame->expand( $text, PPFrame::RECOVER_ORIG );
+ $isLocalObj = false;
+ }
+
+ # Replace raw HTML by a placeholder
+ # Add a blank line preceding, to prevent it from mucking up
+ # immediately preceding headings
+ if ( $isHTML ) {
+ $text = "\n\n" . $this->insertStripItem( $text );
+ }
+ # Escape nowiki-style return values
+ elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) {
+ $text = wfEscapeWikiText( $text );
+ }
+ # Bug 529: if the template begins with a table or block-level
+ # element, it should be treated as beginning a new line.
+ # This behaviour is somewhat controversial.
+ elseif ( is_string( $text ) && !$piece['lineStart'] && preg_match('/^(?:{\\||:|;|#|\*)/', $text)) /*}*/{
+ $text = "\n" . $text;
+ }
+
+ if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
+ # Error, oversize inclusion
+ $text = "[[$originalTitle]]" .
+ $this->insertStripItem( '<!-- WARNING: template omitted, post-expand include size too large -->' );
+ $this->limitationWarn( 'post-expand-template-inclusion' );
+ }
+
+ if ( $isLocalObj ) {
+ $ret = array( 'object' => $text );
+ } else {
+ $ret = array( 'text' => $text );
+ }
+
+ wfProfileOut( $fname );
+ return $ret;
+ }
+
+ /**
+ * Get the semi-parsed DOM representation of a template with a given title,
+ * and its redirect destination title. Cached.
+ */
+ function getTemplateDom( $title ) {
+ $cacheTitle = $title;
+ $titleText = $title->getPrefixedDBkey();
+
+ if ( isset( $this->mTplRedirCache[$titleText] ) ) {
+ list( $ns, $dbk ) = $this->mTplRedirCache[$titleText];
+ $title = Title::makeTitle( $ns, $dbk );
+ $titleText = $title->getPrefixedDBkey();
+ }
+ if ( isset( $this->mTplDomCache[$titleText] ) ) {
+ return array( $this->mTplDomCache[$titleText], $title );
+ }
+
+ // Cache miss, go to the database
+ list( $text, $title ) = $this->fetchTemplateAndTitle( $title );
+
+ if ( $text === false ) {
+ $this->mTplDomCache[$titleText] = false;
+ return array( false, $title );
+ }
+
+ $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
+ $this->mTplDomCache[ $titleText ] = $dom;
+
+ if (! $title->equals($cacheTitle)) {
+ $this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] =
+ array( $title->getNamespace(),$cdb = $title->getDBkey() );
+ }
+
+ return array( $dom, $title );
+ }
+
+ /**
+ * 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().
+ */
+ 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;
+ // If there is no current revision, there is no page
+ if( $id === false && !$rev ) {
+ $linkCache = LinkCache::singleton();
+ $linkCache->addBadLinkObj( $title );
+ }
+
+ $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( $piece, $frame ) {
+ wfProfileIn( __METHOD__ );
+
+ $error = false;
+ $parts = $piece['parts'];
+ $nameWithSpaces = $frame->expand( $piece['title'] );
+ $argName = trim( $nameWithSpaces );
+ $object = false;
+ $text = $frame->getArgument( $argName );
+ if ( $text === false && $parts->getLength() > 0
+ && (
+ $this->ot['html']
+ || $this->ot['pre']
+ || ( $this->ot['wiki'] && $frame->isTemplate() )
+ )
+ ) {
+ # No match in frame, use the supplied default
+ $object = $parts->item( 0 )->getChildren();
+ }
+ if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
+ $error = '<!-- WARNING: argument omitted, expansion size too large -->';
+ $this->limitationWarn( 'post-expand-template-argument' );
+ }
+
+ if ( $text === false && $object === false ) {
+ # No match anywhere
+ $object = $frame->virtualBracketedImplode( '{{{', '|', '}}}', $nameWithSpaces, $parts );
+ }
+ if ( $error !== false ) {
+ $text .= $error;
+ }
+ if ( $object !== false ) {
+ $ret = array( 'object' => $object );
+ } else {
+ $ret = array( 'text' => $text );
+ }
+
+ wfProfileOut( __METHOD__ );
+ return $ret;
+ }
+
+ /**
+ * Return the text to be used for a given extension tag.
+ * This is the ghost of strip().
+ *
+ * @param array $params Associative array of parameters:
+ * name PPNode for the tag name
+ * attr PPNode for unparsed text where tag attributes are thought to be
+ * attributes Optional associative array of parsed attributes
+ * inner Contents of extension element
+ * noClose Original text did not have a close tag
+ * @param PPFrame $frame
+ */
+ function extensionSubstitution( $params, $frame ) {
+ global $wgRawHtml, $wgContLang;
+
+ $name = $frame->expand( $params['name'] );
+ $attrText = !isset( $params['attr'] ) ? null : $frame->expand( $params['attr'] );
+ $content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] );
+
+ $marker = "{$this->mUniqPrefix}-$name-" . sprintf('%08X', $this->mMarkerIndex++) . self::MARKER_SUFFIX;
+
+ if ( $this->ot['html'] ) {
+ $name = strtolower( $name );
+
+ $attributes = Sanitizer::decodeTagAttributes( $attrText );
+ if ( isset( $params['attributes'] ) ) {
+ $attributes = $attributes + $params['attributes'];
+ }
+ switch ( $name ) {
+ case 'html':
+ if( $wgRawHtml ) {
+ $output = $content;
+ break;
+ } else {
+ throw new MWException( '<html> extension tag encountered unexpectedly' );
+ }
+ case 'nowiki':
+ $output = Xml::escapeTagsOnly( $content );
+ break;
+ case 'math':
+ $output = $wgContLang->armourMath(
+ MathRenderer::renderMath( $content, $attributes ) );
+ break;
+ case 'gallery':
+ $output = $this->renderImageGallery( $content, $attributes );
+ break;
+ default:
+ if( isset( $this->mTagHooks[$name] ) ) {
+ # Workaround for PHP bug 35229 and similar
+ if ( !is_callable( $this->mTagHooks[$name] ) ) {
+ throw new MWException( "Tag hook for $name is not callable\n" );
+ }
+ $output = call_user_func_array( $this->mTagHooks[$name],
+ array( $content, $attributes, $this ) );
+ } else {
+ $output = '<span class="error">Invalid tag extension name: ' .
+ htmlspecialchars( $name ) . '</span>';
+ }
+ }
+ } else {
+ if ( is_null( $attrText ) ) {
+ $attrText = '';
+ }
+ if ( isset( $params['attributes'] ) ) {
+ foreach ( $params['attributes'] as $attrName => $attrValue ) {
+ $attrText .= ' ' . htmlspecialchars( $attrName ) . '="' .
+ htmlspecialchars( $attrValue ) . '"';
+ }
+ }
+ if ( $content === null ) {
+ $output = "<$name$attrText/>";
+ } else {
+ $close = is_null( $params['close'] ) ? '' : $frame->expand( $params['close'] );
+ $output = "<$name$attrText>$content$close";
+ }
+ }
+
+ if ( $name == 'html' || $name == 'nowiki' ) {
+ $this->mStripState->nowiki->setPair( $marker, $output );
+ } else {
+ $this->mStripState->general->setPair( $marker, $output );
+ }
+ return $marker;
+ }
+
+ /**
+ * 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( $type ) ) {
+ return false;
+ } else {
+ $this->mIncludeSizes[$type] += $size;
+ return true;
+ }
+ }
+
+ /**
+ * Increment the expensive function count
+ *
+ * @return boolean False if the limit has been exceeded
+ */
+ function incrementExpensiveFunctionCount() {
+ global $wgExpensiveParserFunctionLimit;
+ $this->mExpensiveFunctionCount++;
+ if($this->mExpensiveFunctionCount <= $wgExpensiveParserFunctionLimit) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Strip double-underscore items like __NOGALLERY__ and __NOTOC__
+ * Fills $this->mDoubleUnderscores, returns the modified text
+ */
+ function doDoubleUnderscore( $text ) {
+ // The position of __TOC__ needs to be recorded
+ $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 );
+ }
+
+ // Now match and remove the rest of them
+ $mwa = MagicWord::getDoubleUnderscoreArray();
+ $this->mDoubleUnderscores = $mwa->matchAndRemove( $text );
+
+ if ( isset( $this->mDoubleUnderscores['nogallery'] ) ) {
+ $this->mOutput->mNoGallery = true;
+ }
+ if ( isset( $this->mDoubleUnderscores['notoc'] ) && !$this->mForceTocPosition ) {
+ $this->mShowToc = false;
+ }
+ if ( isset( $this->mDoubleUnderscores['hiddencat'] ) && $this->mTitle->getNamespace() == NS_CATEGORY ) {
+ $this->mOutput->setProperty( 'hiddencat', 'y' );
+
+ $containerCategory = Title::makeTitleSafe( NS_CATEGORY, wfMsgForContent( 'hidden-category-category' ) );
+ if ( $containerCategory ) {
+ $this->mOutput->addCategory( $containerCategory->getDBkey(), $this->getDefaultSort() );
+ } else {
+ wfDebug( __METHOD__.": [[MediaWiki:hidden-category-category]] is not a valid title!\n" );
+ }
+ }
+ 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
+ if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
+ $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__
+ if ( isset( $this->mDoubleUnderscores['newsectionlink'] ) ) {
+ $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
+ if ( isset( $this->mDoubleUnderscores['forcetoc'] ) ) {
+ $this->mShowToc = true;
+ $enoughToc = true;
+ }
+
+ # We need this to perform operations on the HTML
+ $sk = $this->mOptions->getSkin();
+
+ # headline counter
+ $headlineCount = 0;
+ $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;
+ $markerRegex = "{$this->mUniqPrefix}-h-(\d+)-" . self::MARKER_SUFFIX;
+ $baseTitleText = $this->mTitle->getPrefixedDBkey();
+ $tocraw = array();
+
+ foreach( $matches[3] as $headline ) {
+ $isTemplate = false;
+ $titleText = false;
+ $sectionIndex = false;
+ $numbering = '';
+ $markerMatches = array();
+ if (preg_match("/^$markerRegex/", $headline, $markerMatches)) {
+ $serial = $markerMatches[1];
+ list( $titleText, $sectionIndex ) = $this->mHeadings[$serial];
+ $isTemplate = ($titleText != $baseTitleText);
+ $headline = preg_replace("/^$markerRegex/", "", $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 );
+ $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 safe 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
+ $safeHeadline = $this->mStripState->unstripBoth( $headline );
+
+ # Remove link placeholders by the link text.
+ # <!--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 );
+
+ # Strip out HTML (other than plain <sup> and <sub>: bug 8393)
+ $tocline = preg_replace(
+ array( '#<(?!/?(sup|sub)).*?'.'>#', '#<(/?(sup|sub)).*?'.'>#' ),
+ array( '', '<$1>'),
+ $safeHeadline
+ );
+ $tocline = trim( $tocline );
+
+ # For the anchor, strip out HTML-y stuff period
+ $safeHeadline = preg_replace( '/<.*?'.'>/', '', $safeHeadline );
+ $safeHeadline = trim( $safeHeadline );
+
+ # 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)
+ $arrayKey = strtolower( $safeHeadline );
+
+ # 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];
+
+ # 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 = $safeHeadline;
+ 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 && $sectionIndex !== false ) {
+ 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");
+ } else {
+ $editlink = $sk->editSectionLink($this->mTitle, $sectionIndex, $headlineHint);
+ }
+ } else {
+ $editlink = '';
+ }
+ $head[$headlineCount] = $sk->makeHeadline( $level, $matches['attrib'][$headlineCount], $anchor, $headline, $editlink );
+
+ $headlineCount++;
+ }
+
+ $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->setTitle( $title );
+ $this->setOutputType( self::OT_WIKI );
+
+ if ( $clearState ) {
+ $this->clearState();
+ }
+
+ $pairs = array(
+ "\r\n" => "\n",
+ );
+ $text = str_replace( array_keys( $pairs ), array_values( $pairs ), $text );
+ $text = $this->pstPass2( $text, $user );
+ $text = $this->mStripState->unstripBoth( $text );
+ return $text;
+ }
+
+ /**
+ * Pre-save transform helper function
+ * @private
+ */
+ function pstPass2( $text, $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.
+ *
+ * (see also bug 12815)
+ */
+ $ts = $this->mOptions->getTimestamp();
+ $tz = wfMsgForContent( 'timezone-utc' );
+ if ( isset( $wgLocaltimezone ) ) {
+ $unixts = wfTimestamp( TS_UNIX, $ts );
+ $oldtz = getenv( 'TZ' );
+ putenv( 'TZ='.$wgLocaltimezone );
+ $ts = date( 'YmdHis', $unixts );
+ $tz = date( 'T', $unixts ); # might vary on DST changeover!
+ putenv( 'TZ='.$oldtz );
+ }
+
+ $d = $wgContLang->timeanddate( $ts, false, false ) . " ($tz)";
+
+ # Variable replacement
+ # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
+ $text = $this->replaceVariables( $text );
+
+ # 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 ) {
+ if ( !$parsing ) {
+ global $wgTitle;
+ $this->clearState();
+ $this->setTitle( $wgTitle );
+ $this->mOptions = new ParserOptions;
+ $this->setOutputType = self::OT_PREPROCESS;
+ }
+
+ # FIXME: regex doesn't respect extension tags or nowiki
+ # => Move this logic to braceSubstitution()
+ $substWord = MagicWord::get( 'subst' );
+ $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
+ $substText = '{{' . $substWord->getSynonym( 0 );
+
+ $text = preg_replace( $substRegex, $substText, $text );
+ $text = $this->cleanSigInSig( $text );
+ $dom = $this->preprocessToDom( $text );
+ $frame = $this->getPreprocessor()->newFrame();
+ $text = $frame->expand( $dom );
+
+ if ( !$parsing ) {
+ $text = $this->mStripState->unstripBoth( $text );
+ }
+
+ 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->setTitle( $title );
+ $this->mOptions = $options;
+ $this->setOutputType( $outputType );
+ if ( $clearState ) {
+ $this->clearState();
+ }
+ }
+
+ /**
+ * Wrapper for preprocess()
+ *
+ * @param string $text the text to preprocess
+ * @param ParserOptions $options options
+ * @return string
+ * @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);
+ $text = $this->preprocess( $text, $wgTitle, $options );
+
+ $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;
+ if( !in_array( $tag, $this->mStripList ) ) {
+ $this->mStripList[] = $tag;
+ }
+
+ return $oldVal;
+ }
+
+ function setTransparentTagHook( $tag, $callback ) {
+ $tag = strtolower( $tag );
+ $oldVal = isset( $this->mTransparentTagHooks[$tag] ) ? $this->mTransparentTagHooks[$tag] : null;
+ $this->mTransparentTagHooks[$tag] = $callback;
+
+ return $oldVal;
+ }
+
+ /**
+ * Remove all tag hooks
+ */
+ function clearTagHooks() {
+ $this->mTagHooks = array();
+ $this->mStripList = $this->mDefaultStripList;
+ }
+
+ /**
+ * Create a function, e.g. {{sum:1|2|3}}
+ * The callback function should have the form:
+ * function myParserFunction( &$parser, $arg1, $arg2, $arg3 ) { ... }
+ *
+ * Or with SFH_OBJECT_ARGS:
+ * function myParserFunction( $parser, $frame, $args ) { ... }
+ *
+ * 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
+ * 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:...}}
+ *
+ * SFH_OBJECT_ARGS Pass the template arguments as PPNode objects instead of text. This
+ * allows for conditional expansion of the parse tree, allowing you to eliminate dead
+ * branches and thus speed up parsing. It is also possible to analyse the parse tree of
+ * the arguments, and to control the way they are expanded.
+ *
+ * The $frame parameter is a PPFrame. This can be used to produce expanded text from the
+ * arguments, for instance:
+ * $text = isset( $args[0] ) ? $frame->expand( $args[0] ) : '';
+ *
+ * For technical reasons, $args[0] is pre-expanded and will be a string. This may change in
+ * future versions. Please call $frame->expand() on it anyway so that your code keeps
+ * working if/when this is changed.
+ *
+ * If you want whitespace to be trimmed from $args, you need to do it yourself, post-
+ * expansion.
+ *
+ * Please read the documentation in includes/parser/Preprocessor.php for more information
+ * about the methods available in PPFrame and PPNode.
+ *
+ * @return The old callback function for this name, if any
+ */
+ function setFunctionHook( $id, $callback, $flags = 0 ) {
+ $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id][0] : null;
+ $this->mFunctionHooks[$id] = array( $callback, $flags );
+
+ # 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 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;
+ }
+
+ /**
+ * 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;
+ }
+
+ if ( strpos( $matches[0], '%' ) !== false )
+ $matches[1] = urldecode( $matches[1] );
+ $tp = Title::newFromText( $matches[1] );
+ $nt =& $tp;
+ if( is_null( $nt ) ) {
+ # Bogus title. Ignore these so we don't bomb out later.
+ continue;
+ }
+ if ( isset( $matches[3] ) ) {
+ $label = $matches[3];
+ } else {
+ $label = '';
+ }
+
+ $html = $this->recursiveTagParse( trim( $label ) );
+
+ $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 ) {
+ # 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 = $descQuery = false;
+ wfRunHooks( 'BeforeParserMakeImageLinkObj', array( &$this, &$title, &$skip, &$time, &$descQuery ) );
+
+ 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 );
+ $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' ) {
+ $m = array();
+ # (bug 13500) In both cases (width/height and width only),
+ # permit trailing "px" for backward compatibility.
+ if ( preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) {
+ $width = intval( $m[1] );
+ $height = intval( $m[2] );
+ if ( $handler->validateParam( 'width', $width ) ) {
+ $params[$type]['width'] = $width;
+ $validated = true;
+ }
+ if ( $handler->validateParam( 'height', $height ) ) {
+ $params[$type]['height'] = $height;
+ $validated = true;
+ }
+ } elseif ( preg_match( '/^[0-9]*\s*(?:px)?\s*$/', $value ) ) {
+ $width = intval( $value );
+ if ( $handler->validateParam( 'width', $width ) ) {
+ $params[$type]['width'] = $width;
+ $validated = true;
+ }
+ } // else no validation -- bug 13436
+ } else {
+ 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.
+ $validated = true;
+ break;
+ default:
+ // Most other things appear to be empty or numeric...
+ $validated = ( $value === false || is_numeric( trim( $value ) ) );
+ }
+ }
+
+ if ( $validated ) {
+ $params[$type][$paramName] = $value;
+ }
+ }
+ }
+ if ( !$validated ) {
+ $caption = $part;
+ }
+ }
+
+ # Process alignment parameters
+ if ( $params['horizAlign'] ) {
+ $params['frame']['align'] = key( $params['horizAlign'] );
+ }
+ if ( $params['vertAlign'] ) {
+ $params['frame']['valign'] = key( $params['vertAlign'] );
+ }
+
+ # 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;
+
+ wfRunHooks( 'ParserMakeImageParams', array( $title, $file, &$params ) );
+
+ # Linker does the rest
+ $ret = $sk->makeImageLink2( $title, $file, $params['frame'], $params['handler'], $time, $descQuery );
+
+ # 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 PPFrame $frame
+ * @return string
+ * @private
+ */
+ function attributeStripCallback( &$text, $frame = false ) {
+ $text = $this->replaceVariables( $text, $frame );
+ $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 string $text Page wikitext
+ * @param string $section A section identifier string of the form:
+ * <flag1> - <flag2> - ... - <section number>
+ *
+ * Currently the only recognised flag is "T", which means the target section number
+ * was derived during a template inclusion parse, in other words this is a template
+ * section edit link. If no flags are given, it was an ordinary section edit link.
+ * This flag is required to avoid a section numbering mismatch when a section is
+ * enclosed by <includeonly> (bug 6563).
+ *
+ * The section number 0 pulls the text before the first heading; other numbers will
+ * pull the given section along with its lower-level subsections. If the section is
+ * not found, $mode=get will return $newtext, and $mode=replace will return $text.
+ *
+ * @param string $mode One of "get" or "replace"
+ * @param string $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='' ) {
+ global $wgTitle;
+ $this->clearState();
+ $this->setTitle( $wgTitle ); // not generally used but removes an ugly failure mode
+ $this->mOptions = new ParserOptions;
+ $this->setOutputType( self::OT_WIKI );
+ $outText = '';
+ $frame = $this->getPreprocessor()->newFrame();
+
+ // Process section extraction flags
+ $flags = 0;
+ $sectionParts = explode( '-', $section );
+ $sectionIndex = array_pop( $sectionParts );
+ foreach ( $sectionParts as $part ) {
+ if ( $part == 'T' ) {
+ $flags |= self::PTD_FOR_INCLUSION;
+ }
+ }
+ // Preprocess the text
+ $root = $this->preprocessToDom( $text, $flags );
+
+ // <h> nodes indicate section breaks
+ // They can only occur at the top level, so we can find them by iterating the root's children
+ $node = $root->getFirstChild();
+
+ // Find the target section
+ if ( $sectionIndex == 0 ) {
+ // Section zero doesn't nest, level=big
+ $targetLevel = 1000;
+ } else {
+ while ( $node ) {
+ if ( $node->getName() == 'h' ) {
+ $bits = $node->splitHeading();
+ if ( $bits['i'] == $sectionIndex ) {
+ $targetLevel = $bits['level'];
+ break;
+ }
+ }
+ if ( $mode == 'replace' ) {
+ $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
+ }
+ $node = $node->getNextSibling();
+ }
+ }
+
+ if ( !$node ) {
+ // Not found
+ if ( $mode == 'get' ) {
+ return $newText;
+ } else {
+ return $text;
+ }
+ }
+
+ // Find the end of the section, including nested sections
+ do {
+ if ( $node->getName() == 'h' ) {
+ $bits = $node->splitHeading();
+ $curLevel = $bits['level'];
+ if ( $bits['i'] != $sectionIndex && $curLevel <= $targetLevel ) {
+ break;
+ }
+ }
+ 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' ) {
+ // 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
+ $outText .= $newText . "\n\n";
+ while ( $node ) {
+ $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
+ $node = $node->getNextSibling();
+ }
+ }
+
+ if ( is_string( $outText ) ) {
+ // Re-insert stripped tags
+ $outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
+ }
+
+ return $outText;
+ }
+
+ /**
+ * 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 string $text text to look in
+ * @param string $section section identifier
+ * @param string $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;
+ }
+
+ function srvus( $text ) {
+ return $this->testSrvus( $text, $this->mOutputType );
+ }
+
+ /**
+ * strip/replaceVariables/unstrip for preprocessor regression testing
+ */
+ function testSrvus( $text, $title, $options, $outputType = self::OT_HTML ) {
+ $this->clearState();
+ if ( ! ( $title instanceof Title ) ) {
+ $title = Title::newFromText( $title );
+ }
+ $this->mTitle = $title;
+ $this->mOptions = $options;
+ $this->setOutputType( $outputType );
+ $text = $this->replaceVariables( $text );
+ $text = $this->mStripState->unstripBoth( $text );
+ $text = Sanitizer::removeHTMLtags( $text );
+ return $text;
+ }
+
+ function testPst( $text, $title, $options ) {
+ global $wgUser;
+ if ( ! ( $title instanceof Title ) ) {
+ $title = Title::newFromText( $title );
+ }
+ return $this->preSaveTransform( $text, $title, $wgUser, $options );
+ }
+
+ function testPreprocess( $text, $title, $options ) {
+ if ( ! ( $title instanceof Title ) ) {
+ $title = Title::newFromText( $title );
+ }
+ return $this->testSrvus( $text, $title, $options, self::OT_PREPROCESS );
+ }
+
+ function markerSkipCallback( $s, $callback ) {
+ $i = 0;
+ $out = '';
+ while ( $i < strlen( $s ) ) {
+ $markerStart = strpos( $s, $this->mUniqPrefix, $i );
+ if ( $markerStart === false ) {
+ $out .= call_user_func( $callback, substr( $s, $i ) );
+ break;
+ } else {
+ $out .= call_user_func( $callback, substr( $s, $i, $markerStart - $i ) );
+ $markerEnd = strpos( $s, self::MARKER_SUFFIX, $markerStart );
+ if ( $markerEnd === false ) {
+ $out .= substr( $s, $markerStart );
+ break;
+ } else {
+ $markerEnd += strlen( self::MARKER_SUFFIX );
+ $out .= substr( $s, $markerStart, $markerEnd - $markerStart );
+ $i = $markerEnd;
+ }
+ }
+ }
+ return $out;
+ }
+}
+
+/**
+ * @todo document, briefly.
+ * @ingroup Parser
+ */
+class StripState {
+ var $general, $nowiki;
+
+ function __construct() {
+ $this->general = new ReplacementArray;
+ $this->nowiki = new ReplacementArray;
+ }
+
+ function unstripGeneral( $text ) {
+ wfProfileIn( __METHOD__ );
+ do {
+ $oldText = $text;
+ $text = $this->general->replace( $text );
+ } while ( $text != $oldText );
+ wfProfileOut( __METHOD__ );
+ return $text;
+ }
+
+ function unstripNoWiki( $text ) {
+ wfProfileIn( __METHOD__ );
+ do {
+ $oldText = $text;
+ $text = $this->nowiki->replace( $text );
+ } while ( $text != $oldText );
+ wfProfileOut( __METHOD__ );
+ return $text;
+ }
+
+ function unstripBoth( $text ) {
+ wfProfileIn( __METHOD__ );
+ do {
+ $oldText = $text;
+ $text = $this->general->replace( $text );
+ $text = $this->nowiki->replace( $text );
+ } while ( $text != $oldText );
+ wfProfileOut( __METHOD__ );
+ return $text;
+ }
+}
+
+/**
+ * @todo document, briefly.
+ * @ingroup Parser
+ */
+class OnlyIncludeReplacer {
+ var $output = '';
+
+ function replace( $matches ) {
+ 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
new file mode 100644
index 00000000..bf11da2e
--- /dev/null
+++ b/includes/parser/ParserCache.php
@@ -0,0 +1,116 @@
+<?php
+/**
+ * @ingroup Cache Parser
+ * @todo document
+ */
+class ParserCache {
+ /**
+ * Get an instance of this object
+ */
+ public static function &singleton() {
+ static $instance;
+ if ( !isset( $instance ) ) {
+ global $parserMemc;
+ $instance = new ParserCache( $parserMemc );
+ }
+ return $instance;
+ }
+
+ /**
+ * Setup a cache pathway with a given back-end storage mechanism.
+ * May be a memcached client or a BagOStuff derivative.
+ *
+ * @param object $memCached
+ */
+ function __construct( &$memCached ) {
+ $this->mMemc =& $memCached;
+ }
+
+ function getKey( &$article, &$user ) {
+ global $action;
+ $hash = $user->getPageRenderingHash();
+ if( !$article->mTitle->quickUserCan( 'edit' ) ) {
+ // section edit links are suppressed even if the user has them on
+ $edit = '!edit=0';
+ } else {
+ $edit = '';
+ }
+ $pageid = intval( $article->getID() );
+ $renderkey = (int)($action == 'render');
+ $key = wfMemcKey( 'pcache', 'idhash', "$pageid-$renderkey!$hash$edit" );
+ return $key;
+ }
+
+ function getETag( &$article, &$user ) {
+ return 'W/"' . $this->getKey($article, $user) . "--" . $article->mTouched. '"';
+ }
+
+ function get( &$article, &$user ) {
+ global $wgCacheEpoch;
+ $fname = 'ParserCache::get';
+ wfProfileIn( $fname );
+
+ $key = $this->getKey( $article, $user );
+
+ wfDebug( "Trying parser cache $key\n" );
+ $value = $this->mMemc->get( $key );
+ if ( is_object( $value ) ) {
+ wfDebug( "Found.\n" );
+ # Delete if article has changed since the cache was made
+ $canCache = $article->checkTouched();
+ $cacheTime = $value->getCacheTime();
+ $touched = $article->mTouched;
+ if ( !$canCache || $value->expired( $touched ) ) {
+ if ( !$canCache ) {
+ wfIncrStats( "pcache_miss_invalid" );
+ wfDebug( "Invalid cached redirect, touched $touched, epoch $wgCacheEpoch, cached $cacheTime\n" );
+ } else {
+ wfIncrStats( "pcache_miss_expired" );
+ wfDebug( "Key expired, touched $touched, epoch $wgCacheEpoch, cached $cacheTime\n" );
+ }
+ $this->mMemc->delete( $key );
+ $value = false;
+ } else {
+ if ( isset( $value->mTimestamp ) ) {
+ $article->mTimestamp = $value->mTimestamp;
+ }
+ wfIncrStats( "pcache_hit" );
+ }
+ } else {
+ wfDebug( "Parser cache miss.\n" );
+ wfIncrStats( "pcache_miss_absent" );
+ $value = false;
+ }
+
+ wfProfileOut( $fname );
+ return $value;
+ }
+
+ function save( $parserOutput, &$article, &$user ){
+ global $wgParserCacheExpireTime;
+ $key = $this->getKey( $article, $user );
+
+ if( $parserOutput->getCacheTime() != -1 ) {
+
+ $now = wfTimestampNow();
+ $parserOutput->setCacheTime( $now );
+
+ // Save the timestamp so that we don't have to load the revision row on view
+ $parserOutput->mTimestamp = $article->getTimestamp();
+
+ $parserOutput->mText .= "\n<!-- Saved in parser cache with key $key and timestamp $now -->\n";
+ wfDebug( "Saved in parser cache with key $key and timestamp $now\n" );
+
+ if( $parserOutput->containsOldMagic() ){
+ $expire = 3600; # 1 hour
+ } else {
+ $expire = $wgParserCacheExpireTime;
+ }
+ $this->mMemc->set( $key, $parserOutput, $expire );
+
+ } else {
+ wfDebug( "Parser output was marked as uncacheable and has not been saved.\n" );
+ }
+ }
+
+}
diff --git a/includes/parser/ParserOptions.php b/includes/parser/ParserOptions.php
new file mode 100644
index 00000000..330ec446
--- /dev/null
+++ b/includes/parser/ParserOptions.php
@@ -0,0 +1,149 @@
+<?php
+
+/**
+ * Set options of the Parser
+ * @todo document
+ * @ingroup Parser
+ */
+class ParserOptions
+{
+ # All variables are supposed to be private in theory, although in practise this is not the case.
+ var $mUseTeX; # Use texvc to expand <math> tags
+ var $mUseDynamicDates; # Use DateFormatter to format dates
+ var $mInterwikiMagic; # Interlanguage links are removed and returned in an array
+ var $mAllowExternalImages; # Allow external images inline
+ var $mAllowExternalImagesFrom; # If not, any exception?
+ var $mSkin; # Reference to the preferred skin
+ var $mDateFormat; # Date format index
+ var $mEditSection; # Create "edit section" links
+ var $mNumberHeadings; # Automatically number headings
+ var $mAllowSpecialInclusion; # Allow inclusion of special pages
+ var $mTidy; # Ask for tidy cleanup
+ var $mInterfaceMessage; # Which lang to call for PLURAL and GRAMMAR
+ var $mTargetLanguage; # Overrides above setting with arbitrary language
+ var $mMaxIncludeSize; # Maximum size of template expansions, in bytes
+ var $mMaxPPNodeCount; # Maximum number of nodes touched by PPFrame::expand()
+ var $mMaxPPExpandDepth; # Maximum recursion depth in PPFrame::expand()
+ var $mMaxTemplateDepth; # Maximum recursion depth for templates within templates
+ var $mRemoveComments; # Remove HTML comments. ONLY APPLIES TO PREPROCESS OPERATIONS
+ 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 $mUser; # Stored user object, just used to initialise the skin
+
+ function getUseTeX() { return $this->mUseTeX; }
+ function getUseDynamicDates() { return $this->mUseDynamicDates; }
+ function getInterwikiMagic() { return $this->mInterwikiMagic; }
+ function getAllowExternalImages() { return $this->mAllowExternalImages; }
+ function getAllowExternalImagesFrom() { return $this->mAllowExternalImagesFrom; }
+ function getEditSection() { return $this->mEditSection; }
+ function getNumberHeadings() { return $this->mNumberHeadings; }
+ function getAllowSpecialInclusion() { return $this->mAllowSpecialInclusion; }
+ function getTidy() { return $this->mTidy; }
+ function getInterfaceMessage() { return $this->mInterfaceMessage; }
+ function getTargetLanguage() { return $this->mTargetLanguage; }
+ function getMaxIncludeSize() { return $this->mMaxIncludeSize; }
+ function getMaxPPNodeCount() { return $this->mMaxPPNodeCount; }
+ function getMaxTemplateDepth() { return $this->mMaxTemplateDepth; }
+ function getRemoveComments() { return $this->mRemoveComments; }
+ function getTemplateCallback() { return $this->mTemplateCallback; }
+ function getEnableLimitReport() { return $this->mEnableLimitReport; }
+
+ function getSkin() {
+ if ( !isset( $this->mSkin ) ) {
+ $this->mSkin = $this->mUser->getSkin();
+ }
+ return $this->mSkin;
+ }
+
+ function getDateFormat() {
+ if ( !isset( $this->mDateFormat ) ) {
+ $this->mDateFormat = $this->mUser->getDatePreference();
+ }
+ return $this->mDateFormat;
+ }
+
+ function getTimestamp() {
+ if ( !isset( $this->mTimestamp ) ) {
+ $this->mTimestamp = wfTimestampNow();
+ }
+ return $this->mTimestamp;
+ }
+
+ function setUseTeX( $x ) { return wfSetVar( $this->mUseTeX, $x ); }
+ function setUseDynamicDates( $x ) { return wfSetVar( $this->mUseDynamicDates, $x ); }
+ function setInterwikiMagic( $x ) { return wfSetVar( $this->mInterwikiMagic, $x ); }
+ function setAllowExternalImages( $x ) { return wfSetVar( $this->mAllowExternalImages, $x ); }
+ function setAllowExternalImagesFrom( $x ) { return wfSetVar( $this->mAllowExternalImagesFrom, $x ); }
+ function setDateFormat( $x ) { return wfSetVar( $this->mDateFormat, $x ); }
+ function setEditSection( $x ) { return wfSetVar( $this->mEditSection, $x ); }
+ function setNumberHeadings( $x ) { return wfSetVar( $this->mNumberHeadings, $x ); }
+ function setAllowSpecialInclusion( $x ) { return wfSetVar( $this->mAllowSpecialInclusion, $x ); }
+ function setTidy( $x ) { return wfSetVar( $this->mTidy, $x); }
+ function setSkin( $x ) { $this->mSkin = $x; }
+ function setInterfaceMessage( $x ) { return wfSetVar( $this->mInterfaceMessage, $x); }
+ function setTargetLanguage( $x ) { return wfSetVar( $this->mTargetLanguage, $x); }
+ function setMaxIncludeSize( $x ) { return wfSetVar( $this->mMaxIncludeSize, $x ); }
+ function setMaxPPNodeCount( $x ) { return wfSetVar( $this->mMaxPPNodeCount, $x ); }
+ function setMaxTemplateDepth( $x ) { return wfSetVar( $this->mMaxTemplateDepth, $x ); }
+ function setRemoveComments( $x ) { return wfSetVar( $this->mRemoveComments, $x ); }
+ 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 __construct( $user = null ) {
+ $this->initialiseFromUser( $user );
+ }
+
+ /**
+ * Get parser options
+ * @static
+ */
+ static function newFromUser( $user ) {
+ return new ParserOptions( $user );
+ }
+
+ /** Get user options */
+ function initialiseFromUser( $userInput ) {
+ global $wgUseTeX, $wgUseDynamicDates, $wgInterwikiMagic, $wgAllowExternalImages;
+ global $wgAllowExternalImagesFrom, $wgAllowSpecialInclusion, $wgMaxArticleSize;
+ global $wgMaxPPNodeCount, $wgMaxTemplateDepth, $wgMaxPPExpandDepth;
+ $fname = 'ParserOptions::initialiseFromUser';
+ wfProfileIn( $fname );
+ if ( !$userInput ) {
+ global $wgUser;
+ if ( isset( $wgUser ) ) {
+ $user = $wgUser;
+ } else {
+ $user = new User;
+ }
+ } else {
+ $user =& $userInput;
+ }
+
+ $this->mUser = $user;
+
+ $this->mUseTeX = $wgUseTeX;
+ $this->mUseDynamicDates = $wgUseDynamicDates;
+ $this->mInterwikiMagic = $wgInterwikiMagic;
+ $this->mAllowExternalImages = $wgAllowExternalImages;
+ $this->mAllowExternalImagesFrom = $wgAllowExternalImagesFrom;
+ $this->mSkin = null; # Deferred
+ $this->mDateFormat = null; # Deferred
+ $this->mEditSection = true;
+ $this->mNumberHeadings = $user->getOption( 'numberheadings' );
+ $this->mAllowSpecialInclusion = $wgAllowSpecialInclusion;
+ $this->mTidy = false;
+ $this->mInterfaceMessage = false;
+ $this->mTargetLanguage = null; // default depends on InterfaceMessage setting
+ $this->mMaxIncludeSize = $wgMaxArticleSize * 1024;
+ $this->mMaxPPNodeCount = $wgMaxPPNodeCount;
+ $this->mMaxPPExpandDepth = $wgMaxPPExpandDepth;
+ $this->mMaxTemplateDepth = $wgMaxTemplateDepth;
+ $this->mRemoveComments = true;
+ $this->mTemplateCallback = array( 'Parser', 'statelessFetchTemplate' );
+ $this->mEnableLimitReport = false;
+ wfProfileOut( $fname );
+ }
+}
diff --git a/includes/parser/ParserOutput.php b/includes/parser/ParserOutput.php
new file mode 100644
index 00000000..f98d5641
--- /dev/null
+++ b/includes/parser/ParserOutput.php
@@ -0,0 +1,206 @@
+<?php
+/**
+ * @todo document
+ * @ingroup Parser
+ */
+class ParserOutput
+{
+ var $mText, # The output text
+ $mLanguageLinks, # List of the full text of language links, in the order they appear
+ $mCategories, # Map of category names to sort keys
+ $mContainsOldMagic, # Boolean variable indicating if the input contained variables like {{CURRENTDAY}}
+ $mCacheTime, # Time when this object was generated, or -1 for uncacheable. Used in ParserCache.
+ $mVersion, # Compatibility check
+ $mTitleText, # title text of the chosen language variant
+ $mLinks, # 2-D map of NS/DBK to ID for the links in the document. ID=zero for broken.
+ $mTemplates, # 2-D map of NS/DBK to ID for the template references. ID=zero for broken.
+ $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
+
+ /**
+ * Overridden title for display
+ */
+ private $displayTitle = false;
+
+ function ParserOutput( $text = '', $languageLinks = array(), $categoryLinks = array(),
+ $containsOldMagic = false, $titletext = '' )
+ {
+ $this->mText = $text;
+ $this->mLanguageLinks = $languageLinks;
+ $this->mCategories = $categoryLinks;
+ $this->mContainsOldMagic = $containsOldMagic;
+ $this->mCacheTime = '';
+ $this->mVersion = Parser::VERSION;
+ $this->mTitleText = $titletext;
+ $this->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; }
+ function &getLanguageLinks() { return $this->mLanguageLinks; }
+ function getCategoryLinks() { return array_keys( $this->mCategories ); }
+ function &getCategories() { return $this->mCategories; }
+ function getCacheTime() { return $this->mCacheTime; }
+ function getTitleText() { return $this->mTitleText; }
+ function getSections() { return $this->mSections; }
+ function &getLinks() { return $this->mLinks; }
+ function &getTemplates() { return $this->mTemplates; }
+ function &getImages() { return $this->mImages; }
+ function &getExternalLinks() { return $this->mExternalLinks; }
+ function getNoGallery() { return $this->mNoGallery; }
+ function getSubtitle() { return $this->mSubtitle; }
+ function getOutputHooks() { return (array)$this->mOutputHooks; }
+ function getWarnings() { return array_keys( $this->mWarnings ); }
+
+ function containsOldMagic() { return $this->mContainsOldMagic; }
+ function setText( $text ) { return wfSetVar( $this->mText, $text ); }
+ function setLanguageLinks( $ll ) { return wfSetVar( $this->mLanguageLinks, $ll ); }
+ function setCategoryLinks( $cl ) { return wfSetVar( $this->mCategories, $cl ); }
+ function setContainsOldMagic( $com ) { return wfSetVar( $this->mContainsOldMagic, $com ); }
+ function setCacheTime( $t ) { return wfSetVar( $this->mCacheTime, $t ); }
+ function setTitleText( $t ) { return wfSetVar( $this->mTitleText, $t ); }
+ function setSections( $toc ) { return wfSetVar( $this->mSections, $toc ); }
+
+ function addCategory( $c, $sort ) { $this->mCategories[$c] = $sort; }
+ function addLanguageLink( $t ) { $this->mLanguageLinks[] = $t; }
+ function addExternalLink( $url ) { $this->mExternalLinks[$url] = 1; }
+ function addWarning( $s ) { $this->mWarnings[$s] = 1; }
+
+ function addOutputHook( $hook, $data = false ) {
+ $this->mOutputHooks[] = array( $hook, $data );
+ }
+
+ function setNewSection( $value ) {
+ $this->mNewSection = (bool)$value;
+ }
+ function getNewSection() {
+ return (bool)$this->mNewSection;
+ }
+
+ function addLink( $title, $id = null ) {
+ $ns = $title->getNamespace();
+ $dbk = $title->getDBkey();
+ if ( !isset( $this->mLinks[$ns] ) ) {
+ $this->mLinks[$ns] = array();
+ }
+ if ( is_null( $id ) ) {
+ $id = $title->getArticleID();
+ }
+ $this->mLinks[$ns][$dbk] = $id;
+ }
+
+ function addImage( $name ) {
+ $this->mImages[$name] = 1;
+ }
+
+ function addTemplate( $title, $page_id, $rev_id ) {
+ $ns = $title->getNamespace();
+ $dbk = $title->getDBkey();
+ if ( !isset( $this->mTemplates[$ns] ) ) {
+ $this->mTemplates[$ns] = array();
+ }
+ $this->mTemplates[$ns][$dbk] = $page_id;
+ if ( !isset( $this->mTemplateIds[$ns] ) ) {
+ $this->mTemplateIds[$ns] = array();
+ }
+ $this->mTemplateIds[$ns][$dbk] = $rev_id; // For versioning
+ }
+
+ /**
+ * Return true if this cached output object predates the global or
+ * per-article cache invalidation timestamps, or if it comes from
+ * an incompatible older version.
+ *
+ * @param string $touched the affected article's last touched timestamp
+ * @return bool
+ * @public
+ */
+ function expired( $touched ) {
+ global $wgCacheEpoch;
+ return $this->getCacheTime() == -1 || // parser says it's uncacheable
+ $this->getCacheTime() < $touched ||
+ $this->getCacheTime() <= $wgCacheEpoch ||
+ !isset( $this->mVersion ) ||
+ version_compare( $this->mVersion, Parser::VERSION, "lt" );
+ }
+
+ /**
+ * Add some text to the <head>.
+ * If $tag is set, the section with that tag will only be included once
+ * in a given page.
+ */
+ function addHeadItem( $section, $tag = false ) {
+ if ( $tag !== false ) {
+ $this->mHeadItems[$tag] = $section;
+ } else {
+ $this->mHeadItems[] = $section;
+ }
+ }
+
+ /**
+ * Override the title to be used for display
+ * -- this is assumed to have been validated
+ * (check equal normalisation, etc.)
+ *
+ * @param string $text Desired title text
+ */
+ public function setDisplayTitle( $text ) {
+ $this->displayTitle = $text;
+ }
+
+ /**
+ * Get the title to be used for display
+ *
+ * @return string
+ */
+ public function getDisplayTitle() {
+ return $this->displayTitle;
+ }
+
+ /**
+ * Fairly generic flag setter thingy.
+ */
+ public function setFlag( $flag ) {
+ $this->mFlags[$flag] = true;
+ }
+
+ public function getFlag( $flag ) {
+ return isset( $this->mFlags[$flag] );
+ }
+
+ /**
+ * Set a property to be cached in the DB
+ */
+ public function setProperty( $name, $value ) {
+ $this->mProperties[$name] = $value;
+ }
+
+ public function getProperty( $name ){
+ return isset( $this->mProperties[$name] ) ? $this->mProperties[$name] : false;
+ }
+
+ public function getProperties() {
+ if ( !isset( $this->mProperties ) ) {
+ $this->mProperties = array();
+ }
+ return $this->mProperties;
+ }
+}
diff --git a/includes/parser/Parser_DiffTest.php b/includes/parser/Parser_DiffTest.php
new file mode 100644
index 00000000..be3702cf
--- /dev/null
+++ b/includes/parser/Parser_DiffTest.php
@@ -0,0 +1,87 @@
+<?php
+
+/**
+ * @ingroup Parser
+ */
+class Parser_DiffTest
+{
+ var $parsers, $conf;
+
+ var $dfUniqPrefix;
+
+ function __construct( $conf ) {
+ if ( !isset( $conf['parsers'] ) ) {
+ throw new MWException( __METHOD__ . ': no parsers specified' );
+ }
+ $this->conf = $conf;
+ $this->dtUniqPrefix = "\x7fUNIQ" . Parser::getRandomString();
+ }
+
+ function init() {
+ if ( !is_null( $this->parsers ) ) {
+ return;
+ }
+
+ global $wgHooks;
+ static $doneHook = false;
+ if ( !$doneHook ) {
+ $doneHook = true;
+ $wgHooks['ParserClearState'][] = array( $this, 'onClearState' );
+ }
+
+ foreach ( $this->conf['parsers'] as $i => $parserConf ) {
+ if ( !is_array( $parserConf ) ) {
+ $class = $parserConf;
+ $parserConf = array( 'class' => $parserConf );
+ } else {
+ $class = $parserConf['class'];
+ }
+ $this->parsers[$i] = new $class( $parserConf );
+ }
+ }
+
+ function __call( $name, $args ) {
+ $this->init();
+ $results = array();
+ $mismatch = false;
+ $lastResult = null;
+ $first = true;
+ foreach ( $this->parsers as $i => $parser ) {
+ $currentResult = call_user_func_array( array( &$this->parsers[$i], $name ), $args );
+ if ( $first ) {
+ $first = false;
+ } else {
+ if ( is_object( $lastResult ) ) {
+ if ( $lastResult != $currentResult ) {
+ $mismatch = true;
+ }
+ } else {
+ if ( $lastResult !== $currentResult ) {
+ $mismatch = true;
+ }
+ }
+ }
+ $results[$i] = $currentResult;
+ $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" );
+ }
+ return $lastResult;
+ }
+
+ function setFunctionHook( $id, $callback, $flags = 0 ) {
+ $this->init();
+ foreach ( $this->parsers as $i => $parser ) {
+ $parser->setFunctionHook( $id, $callback, $flags );
+ }
+ }
+
+ function onClearState( &$parser ) {
+ // hack marker prefixes to get identical output
+ $parser->mUniqPrefix = $this->dtUniqPrefix;
+ return true;
+ }
+}
diff --git a/includes/parser/Parser_OldPP.php b/includes/parser/Parser_OldPP.php
new file mode 100644
index 00000000..487d3ffd
--- /dev/null
+++ b/includes/parser/Parser_OldPP.php
@@ -0,0 +1,4944 @@
+<?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.php b/includes/parser/Preprocessor.php
new file mode 100644
index 00000000..1a33ac7f
--- /dev/null
+++ b/includes/parser/Preprocessor.php
@@ -0,0 +1,163 @@
+<?php
+
+/**
+ * @ingroup Parser
+ */
+interface Preprocessor {
+ /** Create a new preprocessor object based on an initialised Parser object */
+ function __construct( $parser );
+
+ /** Create a new top-level frame for expansion of a page */
+ function newFrame();
+
+ /** Create a new custom frame for programmatic use of parameter replacement as used in some extensions */
+ function newCustomFrame( $args );
+
+ /** Preprocess text to a PPNode */
+ function preprocessToObj( $text, $flags = 0 );
+}
+
+/**
+ * @ingroup Parser
+ */
+interface PPFrame {
+ const NO_ARGS = 1;
+ const NO_TEMPLATES = 2;
+ const STRIP_COMMENTS = 4;
+ const NO_IGNORE = 8;
+ const RECOVER_COMMENTS = 16;
+
+ const RECOVER_ORIG = 27; // = 1|2|8|16 no constant expression support in PHP yet
+
+ /**
+ * Create a child frame
+ */
+ function newChild( $args = false, $title = false );
+
+ /**
+ * Expand a document tree node
+ */
+ function expand( $root, $flags = 0 );
+
+ /**
+ * Implode with flags for expand()
+ */
+ function implodeWithFlags( $sep, $flags /*, ... */ );
+
+ /**
+ * Implode with no flags specified
+ */
+ function implode( $sep /*, ... */ );
+
+ /**
+ * Makes an object that, when expand()ed, will be the same as one obtained
+ * with implode()
+ */
+ function virtualImplode( $sep /*, ... */ );
+
+ /**
+ * Virtual implode with brackets
+ */
+ function virtualBracketedImplode( $start, $sep, $end /*, ... */ );
+
+ /**
+ * Returns true if there are no arguments in this frame
+ */
+ function isEmpty();
+
+ /**
+ * Get an argument to this frame by name
+ */
+ function getArgument( $name );
+
+ /**
+ * Returns true if the infinite loop check is OK, false if a loop is detected
+ */
+ function loopCheck( $title );
+
+ /**
+ * Return true if the frame is a template frame
+ */
+ function isTemplate();
+}
+
+/**
+ * There are three types of nodes:
+ * * Tree nodes, which have a name and contain other nodes as children
+ * * Array nodes, which also contain other nodes but aren't considered part of a tree
+ * * Leaf nodes, which contain the actual data
+ *
+ * This interface provides access to the tree structure and to the contents of array nodes,
+ * but it does not provide access to the internal structure of leaf nodes. Access to leaf
+ * data is provided via two means:
+ * * PPFrame::expand(), which provides expanded text
+ * * The PPNode::split*() functions, which provide metadata about certain types of tree node
+ * @ingroup Parser
+ */
+interface PPNode {
+ /**
+ * Get an array-type node containing the children of this node.
+ * Returns false if this is not a tree node.
+ */
+ function getChildren();
+
+ /**
+ * Get the first child of a tree node. False if there isn't one.
+ */
+ function getFirstChild();
+
+ /**
+ * Get the next sibling of any node. False if there isn't one
+ */
+ function getNextSibling();
+
+ /**
+ * Get all children of this tree node which have a given name.
+ * Returns an array-type node, or false if this is not a tree node.
+ */
+ function getChildrenOfType( $type );
+
+
+ /**
+ * Returns the length of the array, or false if this is not an array-type node
+ */
+ function getLength();
+
+ /**
+ * Returns an item of an array-type node
+ */
+ function item( $i );
+
+ /**
+ * Get the name of this node. The following names are defined here:
+ *
+ * h A heading node.
+ * template A double-brace node.
+ * tplarg A triple-brace node.
+ * title The first argument to a template or tplarg node.
+ * part Subsequent arguments to a template or tplarg node.
+ * #nodelist An array-type node
+ *
+ * The subclass may define various other names for tree and leaf nodes.
+ */
+ function getName();
+
+ /**
+ * Split a <part> node into an associative array containing:
+ * name PPNode name
+ * index String index
+ * value PPNode value
+ */
+ function splitArg();
+
+ /**
+ * Split an <ext> node into an associative array containing name, attr, inner and close
+ * All values in the resulting array are PPNodes. Inner and close are optional.
+ */
+ function splitExt();
+
+ /**
+ * Split an <h> node
+ */
+ function splitHeading();
+}
diff --git a/includes/parser/Preprocessor_DOM.php b/includes/parser/Preprocessor_DOM.php
new file mode 100644
index 00000000..34d58967
--- /dev/null
+++ b/includes/parser/Preprocessor_DOM.php
@@ -0,0 +1,1421 @@
+<?php
+
+/**
+ * @ingroup Parser
+ */
+class Preprocessor_DOM implements Preprocessor {
+ var $parser, $memoryLimit;
+
+ function __construct( $parser ) {
+ $this->parser = $parser;
+ $mem = ini_get( 'memory_limit' );
+ $this->memoryLimit = false;
+ if ( strval( $mem ) !== '' && $mem != -1 ) {
+ if ( preg_match( '/^\d+$/', $mem ) ) {
+ $this->memoryLimit = $mem;
+ } elseif ( preg_match( '/^(\d+)M$/i', $mem, $m ) ) {
+ $this->memoryLimit = $m[1] * 1048576;
+ }
+ }
+ }
+
+ function newFrame() {
+ return new PPFrame_DOM( $this );
+ }
+
+ function newCustomFrame( $args ) {
+ return new PPCustomFrame_DOM( $this, $args );
+ }
+
+ function memCheck() {
+ if ( $this->memoryLimit === false ) {
+ return;
+ }
+ $usage = memory_get_usage();
+ if ( $usage > $this->memoryLimit * 0.9 ) {
+ $limit = intval( $this->memoryLimit * 0.9 / 1048576 + 0.5 );
+ throw new MWException( "Preprocessor hit 90% memory limit ($limit MB)" );
+ }
+ return $usage <= $this->memoryLimit * 0.8;
+ }
+
+ /**
+ * Preprocess some wikitext and return the document tree.
+ * This is the ghost of Parser::replace_variables().
+ *
+ * @param string $text The text to parse
+ * @param integer flags Bitwise combination of:
+ * Parser::PTD_FOR_INCLUSION Handle <noinclude>/<includeonly> as if the text is being
+ * included. Default is to assume a direct page view.
+ *
+ * The generated DOM tree must depend only on the input text and the flags.
+ * The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of bug 4899.
+ *
+ * Any flag added to the $flags parameter here, or any other parameter liable to cause a
+ * change in the DOM tree for a given text, must be passed through the section identifier
+ * in the section edit link and thus back to extractSections().
+ *
+ * The output of this function is currently only cached in process memory, but a persistent
+ * cache may be implemented at a later date which takes further advantage of these strict
+ * dependency requirements.
+ *
+ * @private
+ */
+ function preprocessToObj( $text, $flags = 0 ) {
+ wfProfileIn( __METHOD__ );
+ wfProfileIn( __METHOD__.'-makexml' );
+
+ $rules = array(
+ '{' => array(
+ 'end' => '}',
+ 'names' => array(
+ 2 => 'template',
+ 3 => 'tplarg',
+ ),
+ 'min' => 2,
+ 'max' => 3,
+ ),
+ '[' => array(
+ 'end' => ']',
+ 'names' => array( 2 => null ),
+ 'min' => 2,
+ 'max' => 2,
+ )
+ );
+
+ $forInclusion = $flags & Parser::PTD_FOR_INCLUSION;
+
+ $xmlishElements = $this->parser->getStripList();
+ $enableOnlyinclude = false;
+ if ( $forInclusion ) {
+ $ignoredTags = array( 'includeonly', '/includeonly' );
+ $ignoredElements = array( 'noinclude' );
+ $xmlishElements[] = 'noinclude';
+ if ( strpos( $text, '<onlyinclude>' ) !== false && strpos( $text, '</onlyinclude>' ) !== false ) {
+ $enableOnlyinclude = true;
+ }
+ } else {
+ $ignoredTags = array( 'noinclude', '/noinclude', 'onlyinclude', '/onlyinclude' );
+ $ignoredElements = array( 'includeonly' );
+ $xmlishElements[] = 'includeonly';
+ }
+ $xmlishRegex = implode( '|', array_merge( $xmlishElements, $ignoredTags ) );
+
+ // Use "A" modifier (anchored) instead of "^", because ^ doesn't work with an offset
+ $elementsRegex = "~($xmlishRegex)(?:\s|\/>|>)|(!--)~iA";
+
+ $stack = new PPDStack;
+
+ $searchBase = "[{<\n"; #}
+ $revText = strrev( $text ); // For fast reverse searches
+
+ $i = 0; # Input pointer, starts out pointing to a pseudo-newline before the start
+ $accum =& $stack->getAccum(); # Current accumulator
+ $accum = '<root>';
+ $findEquals = false; # True to find equals signs in arguments
+ $findPipe = false; # True to take notice of pipe characters
+ $headingIndex = 1;
+ $inHeading = false; # True if $i is inside a possible heading
+ $noMoreGT = false; # True if there are no more greater-than (>) signs right of $i
+ $findOnlyinclude = $enableOnlyinclude; # True to ignore all input up to the next <onlyinclude>
+ $fakeLineStart = true; # Do a line-start run without outputting an LF character
+
+ while ( true ) {
+ //$this->memCheck();
+
+ if ( $findOnlyinclude ) {
+ // Ignore all input up to the next <onlyinclude>
+ $startPos = strpos( $text, '<onlyinclude>', $i );
+ if ( $startPos === false ) {
+ // Ignored section runs to the end
+ $accum .= '<ignore>' . htmlspecialchars( substr( $text, $i ) ) . '</ignore>';
+ break;
+ }
+ $tagEndPos = $startPos + strlen( '<onlyinclude>' ); // past-the-end
+ $accum .= '<ignore>' . htmlspecialchars( substr( $text, $i, $tagEndPos - $i ) ) . '</ignore>';
+ $i = $tagEndPos;
+ $findOnlyinclude = false;
+ }
+
+ if ( $fakeLineStart ) {
+ $found = 'line-start';
+ $curChar = '';
+ } else {
+ # Find next opening brace, closing brace or pipe
+ $search = $searchBase;
+ if ( $stack->top === false ) {
+ $currentClosing = '';
+ } else {
+ $currentClosing = $stack->top->close;
+ $search .= $currentClosing;
+ }
+ if ( $findPipe ) {
+ $search .= '|';
+ }
+ if ( $findEquals ) {
+ // First equals will be for the template
+ $search .= '=';
+ }
+ $rule = null;
+ # Output literal section, advance input counter
+ $literalLength = strcspn( $text, $search, $i );
+ if ( $literalLength > 0 ) {
+ $accum .= htmlspecialchars( substr( $text, $i, $literalLength ) );
+ $i += $literalLength;
+ }
+ if ( $i >= strlen( $text ) ) {
+ if ( $currentClosing == "\n" ) {
+ // Do a past-the-end run to finish off the heading
+ $curChar = '';
+ $found = 'line-end';
+ } else {
+ # All done
+ break;
+ }
+ } else {
+ $curChar = $text[$i];
+ if ( $curChar == '|' ) {
+ $found = 'pipe';
+ } elseif ( $curChar == '=' ) {
+ $found = 'equals';
+ } elseif ( $curChar == '<' ) {
+ $found = 'angle';
+ } elseif ( $curChar == "\n" ) {
+ if ( $inHeading ) {
+ $found = 'line-end';
+ } else {
+ $found = 'line-start';
+ }
+ } elseif ( $curChar == $currentClosing ) {
+ $found = 'close';
+ } elseif ( isset( $rules[$curChar] ) ) {
+ $found = 'open';
+ $rule = $rules[$curChar];
+ } else {
+ # Some versions of PHP have a strcspn which stops on null characters
+ # Ignore and continue
+ ++$i;
+ continue;
+ }
+ }
+ }
+
+ if ( $found == 'angle' ) {
+ $matches = false;
+ // Handle </onlyinclude>
+ if ( $enableOnlyinclude && substr( $text, $i, strlen( '</onlyinclude>' ) ) == '</onlyinclude>' ) {
+ $findOnlyinclude = true;
+ continue;
+ }
+
+ // Determine element name
+ if ( !preg_match( $elementsRegex, $text, $matches, 0, $i + 1 ) ) {
+ // Element name missing or not listed
+ $accum .= '&lt;';
+ ++$i;
+ continue;
+ }
+ // Handle comments
+ if ( isset( $matches[2] ) && $matches[2] == '!--' ) {
+ // To avoid leaving blank lines, when a comment is both preceded
+ // and followed by a newline (ignoring spaces), trim leading and
+ // trailing spaces and one of the newlines.
+
+ // Find the end
+ $endPos = strpos( $text, '-->', $i + 4 );
+ if ( $endPos === false ) {
+ // Unclosed comment in input, runs to end
+ $inner = substr( $text, $i );
+ $accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>';
+ $i = strlen( $text );
+ } else {
+ // Search backwards for leading whitespace
+ $wsStart = $i ? ( $i - strspn( $revText, ' ', strlen( $text ) - $i ) ) : 0;
+ // Search forwards for trailing whitespace
+ // $wsEnd will be the position of the last space
+ $wsEnd = $endPos + 2 + strspn( $text, ' ', $endPos + 3 );
+ // Eat the line if possible
+ // TODO: This could theoretically be done if $wsStart == 0, i.e. for comments at
+ // the overall start. That's not how Sanitizer::removeHTMLcomments() did it, but
+ // it's a possible beneficial b/c break.
+ if ( $wsStart > 0 && substr( $text, $wsStart - 1, 1 ) == "\n"
+ && substr( $text, $wsEnd + 1, 1 ) == "\n" )
+ {
+ $startPos = $wsStart;
+ $endPos = $wsEnd + 1;
+ // Remove leading whitespace from the end of the accumulator
+ // Sanity check first though
+ $wsLength = $i - $wsStart;
+ if ( $wsLength > 0 && substr( $accum, -$wsLength ) === str_repeat( ' ', $wsLength ) ) {
+ $accum = substr( $accum, 0, -$wsLength );
+ }
+ // Do a line-start run next time to look for headings after the comment
+ $fakeLineStart = true;
+ } else {
+ // No line to eat, just take the comment itself
+ $startPos = $i;
+ $endPos += 2;
+ }
+
+ if ( $stack->top ) {
+ $part = $stack->top->getCurrentPart();
+ if ( isset( $part->commentEnd ) && $part->commentEnd == $wsStart - 1 ) {
+ // Comments abutting, no change in visual end
+ $part->commentEnd = $wsEnd;
+ } else {
+ $part->visualEnd = $wsStart;
+ $part->commentEnd = $endPos;
+ }
+ }
+ $i = $endPos + 1;
+ $inner = substr( $text, $startPos, $endPos - $startPos + 1 );
+ $accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>';
+ }
+ continue;
+ }
+ $name = $matches[1];
+ $lowerName = strtolower( $name );
+ $attrStart = $i + strlen( $name ) + 1;
+
+ // Find end of tag
+ $tagEndPos = $noMoreGT ? false : strpos( $text, '>', $attrStart );
+ if ( $tagEndPos === false ) {
+ // Infinite backtrack
+ // Disable tag search to prevent worst-case O(N^2) performance
+ $noMoreGT = true;
+ $accum .= '&lt;';
+ ++$i;
+ continue;
+ }
+
+ // Handle ignored tags
+ if ( in_array( $lowerName, $ignoredTags ) ) {
+ $accum .= '<ignore>' . htmlspecialchars( substr( $text, $i, $tagEndPos - $i + 1 ) ) . '</ignore>';
+ $i = $tagEndPos + 1;
+ continue;
+ }
+
+ $tagStartPos = $i;
+ if ( $text[$tagEndPos-1] == '/' ) {
+ $attrEnd = $tagEndPos - 1;
+ $inner = null;
+ $i = $tagEndPos + 1;
+ $close = '';
+ } else {
+ $attrEnd = $tagEndPos;
+ // Find closing tag
+ if ( preg_match( "/<\/$name\s*>/i", $text, $matches, PREG_OFFSET_CAPTURE, $tagEndPos + 1 ) ) {
+ $inner = substr( $text, $tagEndPos + 1, $matches[0][1] - $tagEndPos - 1 );
+ $i = $matches[0][1] + strlen( $matches[0][0] );
+ $close = '<close>' . htmlspecialchars( $matches[0][0] ) . '</close>';
+ } else {
+ // No end tag -- let it run out to the end of the text.
+ $inner = substr( $text, $tagEndPos + 1 );
+ $i = strlen( $text );
+ $close = '';
+ }
+ }
+ // <includeonly> and <noinclude> just become <ignore> tags
+ if ( in_array( $lowerName, $ignoredElements ) ) {
+ $accum .= '<ignore>' . htmlspecialchars( substr( $text, $tagStartPos, $i - $tagStartPos ) )
+ . '</ignore>';
+ continue;
+ }
+
+ $accum .= '<ext>';
+ if ( $attrEnd <= $attrStart ) {
+ $attr = '';
+ } else {
+ $attr = substr( $text, $attrStart, $attrEnd - $attrStart );
+ }
+ $accum .= '<name>' . htmlspecialchars( $name ) . '</name>' .
+ // Note that the attr element contains the whitespace between name and attribute,
+ // this is necessary for precise reconstruction during pre-save transform.
+ '<attr>' . htmlspecialchars( $attr ) . '</attr>';
+ if ( $inner !== null ) {
+ $accum .= '<inner>' . htmlspecialchars( $inner ) . '</inner>';
+ }
+ $accum .= $close . '</ext>';
+ }
+
+ elseif ( $found == 'line-start' ) {
+ // Is this the start of a heading?
+ // Line break belongs before the heading element in any case
+ if ( $fakeLineStart ) {
+ $fakeLineStart = false;
+ } else {
+ $accum .= $curChar;
+ $i++;
+ }
+
+ $count = strspn( $text, '=', $i, 6 );
+ if ( $count == 1 && $findEquals ) {
+ // DWIM: This looks kind of like a name/value separator
+ // Let's let the equals handler have it and break the potential heading
+ // This is heuristic, but AFAICT the methods for completely correct disambiguation are very complex.
+ } elseif ( $count > 0 ) {
+ $piece = array(
+ 'open' => "\n",
+ 'close' => "\n",
+ 'parts' => array( new PPDPart( str_repeat( '=', $count ) ) ),
+ 'startPos' => $i,
+ 'count' => $count );
+ $stack->push( $piece );
+ $accum =& $stack->getAccum();
+ extract( $stack->getFlags() );
+ $i += $count;
+ }
+ }
+
+ elseif ( $found == 'line-end' ) {
+ $piece = $stack->top;
+ // A heading must be open, otherwise \n wouldn't have been in the search list
+ assert( $piece->open == "\n" );
+ $part = $piece->getCurrentPart();
+ // Search back through the input to see if it has a proper close
+ // Do this using the reversed string since the other solutions (end anchor, etc.) are inefficient
+ $wsLength = strspn( $revText, " \t", strlen( $text ) - $i );
+ $searchStart = $i - $wsLength;
+ if ( isset( $part->commentEnd ) && $searchStart - 1 == $part->commentEnd ) {
+ // Comment found at line end
+ // Search for equals signs before the comment
+ $searchStart = $part->visualEnd;
+ $searchStart -= strspn( $revText, " \t", strlen( $text ) - $searchStart );
+ }
+ $count = $piece->count;
+ $equalsLength = strspn( $revText, '=', strlen( $text ) - $searchStart );
+ if ( $equalsLength > 0 ) {
+ if ( $i - $equalsLength == $piece->startPos ) {
+ // This is just a single string of equals signs on its own line
+ // Replicate the doHeadings behaviour /={count}(.+)={count}/
+ // First find out how many equals signs there really are (don't stop at 6)
+ $count = $equalsLength;
+ if ( $count < 3 ) {
+ $count = 0;
+ } else {
+ $count = min( 6, intval( ( $count - 1 ) / 2 ) );
+ }
+ } else {
+ $count = min( $equalsLength, $count );
+ }
+ if ( $count > 0 ) {
+ // Normal match, output <h>
+ $element = "<h level=\"$count\" i=\"$headingIndex\">$accum</h>";
+ $headingIndex++;
+ } else {
+ // Single equals sign on its own line, count=0
+ $element = $accum;
+ }
+ } else {
+ // No match, no <h>, just pass down the inner text
+ $element = $accum;
+ }
+ // Unwind the stack
+ $stack->pop();
+ $accum =& $stack->getAccum();
+ extract( $stack->getFlags() );
+
+ // Append the result to the enclosing accumulator
+ $accum .= $element;
+ // Note that we do NOT increment the input pointer.
+ // This is because the closing linebreak could be the opening linebreak of
+ // another heading. Infinite loops are avoided because the next iteration MUST
+ // hit the heading open case above, which unconditionally increments the
+ // input pointer.
+ }
+
+ elseif ( $found == 'open' ) {
+ # count opening brace characters
+ $count = strspn( $text, $curChar, $i );
+
+ # we need to add to stack only if opening brace count is enough for one of the rules
+ if ( $count >= $rule['min'] ) {
+ # Add it to the stack
+ $piece = array(
+ 'open' => $curChar,
+ 'close' => $rule['end'],
+ 'count' => $count,
+ 'lineStart' => ($i > 0 && $text[$i-1] == "\n"),
+ );
+
+ $stack->push( $piece );
+ $accum =& $stack->getAccum();
+ extract( $stack->getFlags() );
+ } else {
+ # Add literal brace(s)
+ $accum .= htmlspecialchars( str_repeat( $curChar, $count ) );
+ }
+ $i += $count;
+ }
+
+ elseif ( $found == 'close' ) {
+ $piece = $stack->top;
+ # lets check if there are enough characters for closing brace
+ $maxCount = $piece->count;
+ $count = strspn( $text, $curChar, $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;
+ $rule = $rules[$piece->open];
+ if ( $count > $rule['max'] ) {
+ # The specified maximum exists in the callback array, unless the caller
+ # has made an error
+ $matchingCount = $rule['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, $rule['names'] ) ) {
+ --$matchingCount;
+ }
+ }
+
+ if ($matchingCount <= 0) {
+ # No matching element found in callback array
+ # Output a literal closing brace and continue
+ $accum .= htmlspecialchars( str_repeat( $curChar, $count ) );
+ $i += $count;
+ continue;
+ }
+ $name = $rule['names'][$matchingCount];
+ if ( $name === null ) {
+ // No element, just literal text
+ $element = $piece->breakSyntax( $matchingCount ) . str_repeat( $rule['end'], $matchingCount );
+ } else {
+ # Create XML element
+ # Note: $parts is already XML, does not need to be encoded further
+ $parts = $piece->parts;
+ $title = $parts[0]->out;
+ unset( $parts[0] );
+
+ # The invocation is at the start of the line if lineStart is set in
+ # the stack, and all opening brackets are used up.
+ if ( $maxCount == $matchingCount && !empty( $piece->lineStart ) ) {
+ $attr = ' lineStart="1"';
+ } else {
+ $attr = '';
+ }
+
+ $element = "<$name$attr>";
+ $element .= "<title>$title</title>";
+ $argIndex = 1;
+ foreach ( $parts as $partIndex => $part ) {
+ if ( isset( $part->eqpos ) ) {
+ $argName = substr( $part->out, 0, $part->eqpos );
+ $argValue = substr( $part->out, $part->eqpos + 1 );
+ $element .= "<part><name>$argName</name>=<value>$argValue</value></part>";
+ } else {
+ $element .= "<part><name index=\"$argIndex\" /><value>{$part->out}</value></part>";
+ $argIndex++;
+ }
+ }
+ $element .= "</$name>";
+ }
+
+ # Advance input pointer
+ $i += $matchingCount;
+
+ # Unwind the stack
+ $stack->pop();
+ $accum =& $stack->getAccum();
+
+ # Re-add the old stack element if it still has unmatched opening characters remaining
+ if ($matchingCount < $piece->count) {
+ $piece->parts = array( new PPDPart );
+ $piece->count -= $matchingCount;
+ # do we still qualify for any callback with remaining count?
+ $names = $rules[$piece->open]['names'];
+ $skippedBraces = 0;
+ $enclosingAccum =& $accum;
+ while ( $piece->count ) {
+ if ( array_key_exists( $piece->count, $names ) ) {
+ $stack->push( $piece );
+ $accum =& $stack->getAccum();
+ break;
+ }
+ --$piece->count;
+ $skippedBraces ++;
+ }
+ $enclosingAccum .= str_repeat( $piece->open, $skippedBraces );
+ }
+
+ extract( $stack->getFlags() );
+
+ # Add XML element to the enclosing accumulator
+ $accum .= $element;
+ }
+
+ elseif ( $found == 'pipe' ) {
+ $findEquals = true; // shortcut for getFlags()
+ $stack->addPart();
+ $accum =& $stack->getAccum();
+ ++$i;
+ }
+
+ elseif ( $found == 'equals' ) {
+ $findEquals = false; // shortcut for getFlags()
+ $stack->getCurrentPart()->eqpos = strlen( $accum );
+ $accum .= '=';
+ ++$i;
+ }
+ }
+
+ # Output any remaining unclosed brackets
+ foreach ( $stack->stack as $piece ) {
+ $stack->rootAccum .= $piece->breakSyntax();
+ }
+ $stack->rootAccum .= '</root>';
+ $xml = $stack->rootAccum;
+
+ wfProfileOut( __METHOD__.'-makexml' );
+ wfProfileIn( __METHOD__.'-loadXML' );
+ $dom = new DOMDocument;
+ wfSuppressWarnings();
+ $result = $dom->loadXML( $xml );
+ wfRestoreWarnings();
+ if ( !$result ) {
+ // Try running the XML through UtfNormal to get rid of invalid characters
+ $xml = UtfNormal::cleanUp( $xml );
+ $result = $dom->loadXML( $xml );
+ if ( !$result ) {
+ throw new MWException( __METHOD__.' generated invalid XML' );
+ }
+ }
+ $obj = new PPNode_DOM( $dom->documentElement );
+ wfProfileOut( __METHOD__.'-loadXML' );
+ wfProfileOut( __METHOD__ );
+ return $obj;
+ }
+}
+
+/**
+ * Stack class to help Preprocessor::preprocessToObj()
+ * @ingroup Parser
+ */
+class PPDStack {
+ var $stack, $rootAccum, $top;
+ var $out;
+ var $elementClass = 'PPDStackElement';
+
+ static $false = false;
+
+ function __construct() {
+ $this->stack = array();
+ $this->top = false;
+ $this->rootAccum = '';
+ $this->accum =& $this->rootAccum;
+ }
+
+ function count() {
+ return count( $this->stack );
+ }
+
+ function &getAccum() {
+ return $this->accum;
+ }
+
+ function getCurrentPart() {
+ if ( $this->top === false ) {
+ return false;
+ } else {
+ return $this->top->getCurrentPart();
+ }
+ }
+
+ function push( $data ) {
+ if ( $data instanceof $this->elementClass ) {
+ $this->stack[] = $data;
+ } else {
+ $class = $this->elementClass;
+ $this->stack[] = new $class( $data );
+ }
+ $this->top = $this->stack[ count( $this->stack ) - 1 ];
+ $this->accum =& $this->top->getAccum();
+ }
+
+ function pop() {
+ if ( !count( $this->stack ) ) {
+ throw new MWException( __METHOD__.': no elements remaining' );
+ }
+ $temp = array_pop( $this->stack );
+
+ if ( count( $this->stack ) ) {
+ $this->top = $this->stack[ count( $this->stack ) - 1 ];
+ $this->accum =& $this->top->getAccum();
+ } else {
+ $this->top = self::$false;
+ $this->accum =& $this->rootAccum;
+ }
+ return $temp;
+ }
+
+ function addPart( $s = '' ) {
+ $this->top->addPart( $s );
+ $this->accum =& $this->top->getAccum();
+ }
+
+ function getFlags() {
+ if ( !count( $this->stack ) ) {
+ return array(
+ 'findEquals' => false,
+ 'findPipe' => false,
+ 'inHeading' => false,
+ );
+ } else {
+ return $this->top->getFlags();
+ }
+ }
+}
+
+/**
+ * @ingroup Parser
+ */
+class PPDStackElement {
+ var $open, // Opening character (\n for heading)
+ $close, // Matching closing character
+ $count, // Number of opening characters found (number of "=" for heading)
+ $parts, // Array of PPDPart objects describing pipe-separated parts.
+ $lineStart; // True if the open char appeared at the start of the input line. Not set for headings.
+
+ var $partClass = 'PPDPart';
+
+ function __construct( $data = array() ) {
+ $class = $this->partClass;
+ $this->parts = array( new $class );
+
+ foreach ( $data as $name => $value ) {
+ $this->$name = $value;
+ }
+ }
+
+ function &getAccum() {
+ return $this->parts[count($this->parts) - 1]->out;
+ }
+
+ function addPart( $s = '' ) {
+ $class = $this->partClass;
+ $this->parts[] = new $class( $s );
+ }
+
+ function getCurrentPart() {
+ return $this->parts[count($this->parts) - 1];
+ }
+
+ function getFlags() {
+ $partCount = count( $this->parts );
+ $findPipe = $this->open != "\n" && $this->open != '[';
+ return array(
+ 'findPipe' => $findPipe,
+ 'findEquals' => $findPipe && $partCount > 1 && !isset( $this->parts[$partCount - 1]->eqpos ),
+ 'inHeading' => $this->open == "\n",
+ );
+ }
+
+ /**
+ * Get the output string that would result if the close is not found.
+ */
+ function breakSyntax( $openingCount = false ) {
+ if ( $this->open == "\n" ) {
+ $s = $this->parts[0]->out;
+ } else {
+ if ( $openingCount === false ) {
+ $openingCount = $this->count;
+ }
+ $s = str_repeat( $this->open, $openingCount );
+ $first = true;
+ foreach ( $this->parts as $part ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $s .= '|';
+ }
+ $s .= $part->out;
+ }
+ }
+ return $s;
+ }
+}
+
+/**
+ * @ingroup Parser
+ */
+class PPDPart {
+ var $out; // Output accumulator string
+
+ // Optional member variables:
+ // eqpos Position of equals sign in output accumulator
+ // commentEnd Past-the-end input pointer for the last comment encountered
+ // visualEnd Past-the-end input pointer for the end of the accumulator minus comments
+
+ function __construct( $out = '' ) {
+ $this->out = $out;
+ }
+}
+
+/**
+ * An expansion frame, used as a context to expand the result of preprocessToObj()
+ * @ingroup Parser
+ */
+class PPFrame_DOM implements PPFrame {
+ var $preprocessor, $parser, $title;
+ var $titleCache;
+
+ /**
+ * Hashtable listing templates which are disallowed for expansion in this frame,
+ * having been encountered previously in parent frames.
+ */
+ var $loopCheckHash;
+
+ /**
+ * Recursion depth of this frame, top = 0
+ */
+ var $depth;
+
+
+ /**
+ * Construct a new preprocessor frame.
+ * @param Preprocessor $preprocessor The parent preprocessor
+ */
+ function __construct( $preprocessor ) {
+ $this->preprocessor = $preprocessor;
+ $this->parser = $preprocessor->parser;
+ $this->title = $this->parser->mTitle;
+ $this->titleCache = array( $this->title ? $this->title->getPrefixedDBkey() : false );
+ $this->loopCheckHash = array();
+ $this->depth = 0;
+ }
+
+ /**
+ * Create a new child frame
+ * $args is optionally a multi-root PPNode or array containing the template arguments
+ */
+ function newChild( $args = false, $title = false ) {
+ $namedArgs = array();
+ $numberedArgs = array();
+ if ( $title === false ) {
+ $title = $this->title;
+ }
+ if ( $args !== false ) {
+ $xpath = false;
+ if ( $args instanceof PPNode ) {
+ $args = $args->node;
+ }
+ foreach ( $args as $arg ) {
+ if ( !$xpath ) {
+ $xpath = new DOMXPath( $arg->ownerDocument );
+ }
+
+ $nameNodes = $xpath->query( 'name', $arg );
+ $value = $xpath->query( 'value', $arg );
+ if ( $nameNodes->item( 0 )->hasAttributes() ) {
+ // Numbered parameter
+ $index = $nameNodes->item( 0 )->attributes->getNamedItem( 'index' )->textContent;
+ $numberedArgs[$index] = $value->item( 0 );
+ unset( $namedArgs[$index] );
+ } else {
+ // Named parameter
+ $name = trim( $this->expand( $nameNodes->item( 0 ), PPFrame::STRIP_COMMENTS ) );
+ $namedArgs[$name] = $value->item( 0 );
+ unset( $numberedArgs[$name] );
+ }
+ }
+ }
+ return new PPTemplateFrame_DOM( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title );
+ }
+
+ function expand( $root, $flags = 0 ) {
+ static $depth = 0;
+ if ( is_string( $root ) ) {
+ return $root;
+ }
+
+ if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->mMaxPPNodeCount )
+ {
+ return '<span class="error">Node-count limit exceeded</span>';
+ }
+
+ if ( $depth > $this->parser->mOptions->mMaxPPExpandDepth ) {
+ return '<span class="error">Expansion depth limit exceeded</span>';
+ }
+ ++$depth;
+
+ if ( $root instanceof PPNode_DOM ) {
+ $root = $root->node;
+ }
+ if ( $root instanceof DOMDocument ) {
+ $root = $root->documentElement;
+ }
+
+ $outStack = array( '', '' );
+ $iteratorStack = array( false, $root );
+ $indexStack = array( 0, 0 );
+
+ while ( count( $iteratorStack ) > 1 ) {
+ $level = count( $outStack ) - 1;
+ $iteratorNode =& $iteratorStack[ $level ];
+ $out =& $outStack[$level];
+ $index =& $indexStack[$level];
+
+ if ( $iteratorNode instanceof PPNode_DOM ) $iteratorNode = $iteratorNode->node;
+
+ if ( is_array( $iteratorNode ) ) {
+ if ( $index >= count( $iteratorNode ) ) {
+ // All done with this iterator
+ $iteratorStack[$level] = false;
+ $contextNode = false;
+ } else {
+ $contextNode = $iteratorNode[$index];
+ $index++;
+ }
+ } elseif ( $iteratorNode instanceof DOMNodeList ) {
+ if ( $index >= $iteratorNode->length ) {
+ // All done with this iterator
+ $iteratorStack[$level] = false;
+ $contextNode = false;
+ } else {
+ $contextNode = $iteratorNode->item( $index );
+ $index++;
+ }
+ } else {
+ // Copy to $contextNode and then delete from iterator stack,
+ // because this is not an iterator but we do have to execute it once
+ $contextNode = $iteratorStack[$level];
+ $iteratorStack[$level] = false;
+ }
+
+ if ( $contextNode instanceof PPNode_DOM ) $contextNode = $contextNode->node;
+
+ $newIterator = false;
+
+ if ( $contextNode === false ) {
+ // nothing to do
+ } elseif ( is_string( $contextNode ) ) {
+ $out .= $contextNode;
+ } elseif ( is_array( $contextNode ) || $contextNode instanceof DOMNodeList ) {
+ $newIterator = $contextNode;
+ } elseif ( $contextNode instanceof DOMNode ) {
+ if ( $contextNode->nodeType == XML_TEXT_NODE ) {
+ $out .= $contextNode->nodeValue;
+ } elseif ( $contextNode->nodeName == 'template' ) {
+ # Double-brace expansion
+ $xpath = new DOMXPath( $contextNode->ownerDocument );
+ $titles = $xpath->query( 'title', $contextNode );
+ $title = $titles->item( 0 );
+ $parts = $xpath->query( 'part', $contextNode );
+ if ( $flags & self::NO_TEMPLATES ) {
+ $newIterator = $this->virtualBracketedImplode( '{{', '|', '}}', $title, $parts );
+ } else {
+ $lineStart = $contextNode->getAttribute( 'lineStart' );
+ $params = array(
+ 'title' => new PPNode_DOM( $title ),
+ 'parts' => new PPNode_DOM( $parts ),
+ 'lineStart' => $lineStart );
+ $ret = $this->parser->braceSubstitution( $params, $this );
+ if ( isset( $ret['object'] ) ) {
+ $newIterator = $ret['object'];
+ } else {
+ $out .= $ret['text'];
+ }
+ }
+ } elseif ( $contextNode->nodeName == 'tplarg' ) {
+ # Triple-brace expansion
+ $xpath = new DOMXPath( $contextNode->ownerDocument );
+ $titles = $xpath->query( 'title', $contextNode );
+ $title = $titles->item( 0 );
+ $parts = $xpath->query( 'part', $contextNode );
+ if ( $flags & self::NO_ARGS ) {
+ $newIterator = $this->virtualBracketedImplode( '{{{', '|', '}}}', $title, $parts );
+ } else {
+ $params = array(
+ 'title' => new PPNode_DOM( $title ),
+ 'parts' => new PPNode_DOM( $parts ) );
+ $ret = $this->parser->argSubstitution( $params, $this );
+ if ( isset( $ret['object'] ) ) {
+ $newIterator = $ret['object'];
+ } else {
+ $out .= $ret['text'];
+ }
+ }
+ } elseif ( $contextNode->nodeName == 'comment' ) {
+ # HTML-style comment
+ # Remove it in HTML, pre+remove and STRIP_COMMENTS modes
+ if ( $this->parser->ot['html']
+ || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() )
+ || ( $flags & self::STRIP_COMMENTS ) )
+ {
+ $out .= '';
+ }
+ # Add a strip marker in PST mode so that pstPass2() can run some old-fashioned regexes on the result
+ # Not in RECOVER_COMMENTS mode (extractSections) though
+ elseif ( $this->parser->ot['wiki'] && ! ( $flags & self::RECOVER_COMMENTS ) ) {
+ $out .= $this->parser->insertStripItem( $contextNode->textContent );
+ }
+ # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove
+ else {
+ $out .= $contextNode->textContent;
+ }
+ } elseif ( $contextNode->nodeName == 'ignore' ) {
+ # Output suppression used by <includeonly> etc.
+ # OT_WIKI will only respect <ignore> in substed templates.
+ # The other output types respect it unless NO_IGNORE is set.
+ # extractSections() sets NO_IGNORE and so never respects it.
+ if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] ) || ( $flags & self::NO_IGNORE ) ) {
+ $out .= $contextNode->textContent;
+ } else {
+ $out .= '';
+ }
+ } elseif ( $contextNode->nodeName == 'ext' ) {
+ # Extension tag
+ $xpath = new DOMXPath( $contextNode->ownerDocument );
+ $names = $xpath->query( 'name', $contextNode );
+ $attrs = $xpath->query( 'attr', $contextNode );
+ $inners = $xpath->query( 'inner', $contextNode );
+ $closes = $xpath->query( 'close', $contextNode );
+ $params = array(
+ 'name' => new PPNode_DOM( $names->item( 0 ) ),
+ 'attr' => $attrs->length > 0 ? new PPNode_DOM( $attrs->item( 0 ) ) : null,
+ 'inner' => $inners->length > 0 ? new PPNode_DOM( $inners->item( 0 ) ) : null,
+ 'close' => $closes->length > 0 ? new PPNode_DOM( $closes->item( 0 ) ) : null,
+ );
+ $out .= $this->parser->extensionSubstitution( $params, $this );
+ } elseif ( $contextNode->nodeName == 'h' ) {
+ # Heading
+ $s = $this->expand( $contextNode->childNodes, $flags );
+
+ # Insert a heading marker only for <h> children of <root>
+ # This is to stop extractSections from going over multiple tree levels
+ if ( $contextNode->parentNode->nodeName == 'root'
+ && $this->parser->ot['html'] )
+ {
+ # Insert heading index marker
+ $headingIndex = $contextNode->getAttribute( 'i' );
+ $titleText = $this->title->getPrefixedDBkey();
+ $this->parser->mHeadings[] = array( $titleText, $headingIndex );
+ $serial = count( $this->parser->mHeadings ) - 1;
+ $marker = "{$this->parser->mUniqPrefix}-h-$serial-" . Parser::MARKER_SUFFIX;
+ $count = $contextNode->getAttribute( 'level' );
+ $s = substr( $s, 0, $count ) . $marker . substr( $s, $count );
+ $this->parser->mStripState->general->setPair( $marker, '' );
+ }
+ $out .= $s;
+ } else {
+ # Generic recursive expansion
+ $newIterator = $contextNode->childNodes;
+ }
+ } else {
+ throw new MWException( __METHOD__.': Invalid parameter type' );
+ }
+
+ if ( $newIterator !== false ) {
+ if ( $newIterator instanceof PPNode_DOM ) {
+ $newIterator = $newIterator->node;
+ }
+ $outStack[] = '';
+ $iteratorStack[] = $newIterator;
+ $indexStack[] = 0;
+ } elseif ( $iteratorStack[$level] === false ) {
+ // Return accumulated value to parent
+ // With tail recursion
+ while ( $iteratorStack[$level] === false && $level > 0 ) {
+ $outStack[$level - 1] .= $out;
+ array_pop( $outStack );
+ array_pop( $iteratorStack );
+ array_pop( $indexStack );
+ $level--;
+ }
+ }
+ }
+ --$depth;
+ return $outStack[0];
+ }
+
+ function implodeWithFlags( $sep, $flags /*, ... */ ) {
+ $args = array_slice( func_get_args(), 2 );
+
+ $first = true;
+ $s = '';
+ foreach ( $args as $root ) {
+ if ( $root instanceof PPNode_DOM ) $root = $root->node;
+ if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
+ $root = array( $root );
+ }
+ foreach ( $root as $node ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $s .= $sep;
+ }
+ $s .= $this->expand( $node, $flags );
+ }
+ }
+ return $s;
+ }
+
+ /**
+ * Implode with no flags specified
+ * This previously called implodeWithFlags but has now been inlined to reduce stack depth
+ */
+ function implode( $sep /*, ... */ ) {
+ $args = array_slice( func_get_args(), 1 );
+
+ $first = true;
+ $s = '';
+ foreach ( $args as $root ) {
+ if ( $root instanceof PPNode_DOM ) $root = $root->node;
+ if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
+ $root = array( $root );
+ }
+ foreach ( $root as $node ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $s .= $sep;
+ }
+ $s .= $this->expand( $node );
+ }
+ }
+ return $s;
+ }
+
+ /**
+ * Makes an object that, when expand()ed, will be the same as one obtained
+ * with implode()
+ */
+ function virtualImplode( $sep /*, ... */ ) {
+ $args = array_slice( func_get_args(), 1 );
+ $out = array();
+ $first = true;
+ if ( $root instanceof PPNode_DOM ) $root = $root->node;
+
+ foreach ( $args as $root ) {
+ if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
+ $root = array( $root );
+ }
+ foreach ( $root as $node ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $out[] = $sep;
+ }
+ $out[] = $node;
+ }
+ }
+ return $out;
+ }
+
+ /**
+ * Virtual implode with brackets
+ */
+ function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) {
+ $args = array_slice( func_get_args(), 3 );
+ $out = array( $start );
+ $first = true;
+
+ foreach ( $args as $root ) {
+ if ( $root instanceof PPNode_DOM ) $root = $root->node;
+ if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
+ $root = array( $root );
+ }
+ foreach ( $root as $node ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $out[] = $sep;
+ }
+ $out[] = $node;
+ }
+ }
+ $out[] = $end;
+ return $out;
+ }
+
+ function __toString() {
+ return 'frame{}';
+ }
+
+ function getPDBK( $level = false ) {
+ if ( $level === false ) {
+ return $this->title->getPrefixedDBkey();
+ } else {
+ return isset( $this->titleCache[$level] ) ? $this->titleCache[$level] : false;
+ }
+ }
+
+ /**
+ * Returns true if there are no arguments in this frame
+ */
+ function isEmpty() {
+ return true;
+ }
+
+ function getArgument( $name ) {
+ return false;
+ }
+
+ /**
+ * Returns true if the infinite loop check is OK, false if a loop is detected
+ */
+ function loopCheck( $title ) {
+ return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] );
+ }
+
+ /**
+ * Return true if the frame is a template frame
+ */
+ function isTemplate() {
+ return false;
+ }
+}
+
+/**
+ * Expansion frame with template arguments
+ * @ingroup Parser
+ */
+class PPTemplateFrame_DOM extends PPFrame_DOM {
+ var $numberedArgs, $namedArgs, $parent;
+ var $numberedExpansionCache, $namedExpansionCache;
+
+ function __construct( $preprocessor, $parent = false, $numberedArgs = array(), $namedArgs = array(), $title = false ) {
+ $this->preprocessor = $preprocessor;
+ $this->parser = $preprocessor->parser;
+ $this->parent = $parent;
+ $this->numberedArgs = $numberedArgs;
+ $this->namedArgs = $namedArgs;
+ $this->title = $title;
+ $pdbk = $title ? $title->getPrefixedDBkey() : false;
+ $this->titleCache = $parent->titleCache;
+ $this->titleCache[] = $pdbk;
+ $this->loopCheckHash = /*clone*/ $parent->loopCheckHash;
+ if ( $pdbk !== false ) {
+ $this->loopCheckHash[$pdbk] = true;
+ }
+ $this->depth = $parent->depth + 1;
+ $this->numberedExpansionCache = $this->namedExpansionCache = array();
+ }
+
+ function __toString() {
+ $s = 'tplframe{';
+ $first = true;
+ $args = $this->numberedArgs + $this->namedArgs;
+ foreach ( $args as $name => $value ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $s .= ', ';
+ }
+ $s .= "\"$name\":\"" .
+ str_replace( '"', '\\"', $value->ownerDocument->saveXML( $value ) ) . '"';
+ }
+ $s .= '}';
+ return $s;
+ }
+ /**
+ * Returns true if there are no arguments in this frame
+ */
+ function isEmpty() {
+ return !count( $this->numberedArgs ) && !count( $this->namedArgs );
+ }
+
+ function getNumberedArgument( $index ) {
+ if ( !isset( $this->numberedArgs[$index] ) ) {
+ return false;
+ }
+ if ( !isset( $this->numberedExpansionCache[$index] ) ) {
+ # No trimming for unnamed arguments
+ $this->numberedExpansionCache[$index] = $this->parent->expand( $this->numberedArgs[$index], self::STRIP_COMMENTS );
+ }
+ return $this->numberedExpansionCache[$index];
+ }
+
+ function getNamedArgument( $name ) {
+ if ( !isset( $this->namedArgs[$name] ) ) {
+ return false;
+ }
+ if ( !isset( $this->namedExpansionCache[$name] ) ) {
+ # Trim named arguments post-expand, for backwards compatibility
+ $this->namedExpansionCache[$name] = trim(
+ $this->parent->expand( $this->namedArgs[$name], self::STRIP_COMMENTS ) );
+ }
+ return $this->namedExpansionCache[$name];
+ }
+
+ function getArgument( $name ) {
+ $text = $this->getNumberedArgument( $name );
+ if ( $text === false ) {
+ $text = $this->getNamedArgument( $name );
+ }
+ return $text;
+ }
+
+ /**
+ * Return true if the frame is a template frame
+ */
+ function isTemplate() {
+ return true;
+ }
+}
+
+/**
+ * Expansion frame with custom arguments
+ * @ingroup Parser
+ */
+class PPCustomFrame_DOM extends PPFrame_DOM {
+ var $args;
+
+ function __construct( $preprocessor, $args ) {
+ $this->preprocessor = $preprocessor;
+ $this->parser = $preprocessor->parser;
+ $this->args = $args;
+ }
+
+ function __toString() {
+ $s = 'cstmframe{';
+ $first = true;
+ foreach ( $this->args as $name => $value ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $s .= ', ';
+ }
+ $s .= "\"$name\":\"" .
+ str_replace( '"', '\\"', $value->__toString() ) . '"';
+ }
+ $s .= '}';
+ return $s;
+ }
+
+ function isEmpty() {
+ return !count( $this->args );
+ }
+
+ function getArgument( $index ) {
+ return $this->args[$index];
+ }
+}
+
+/**
+ * @ingroup Parser
+ */
+class PPNode_DOM implements PPNode {
+ var $node;
+
+ function __construct( $node, $xpath = false ) {
+ $this->node = $node;
+ }
+
+ function __get( $name ) {
+ if ( $name == 'xpath' ) {
+ $this->xpath = new DOMXPath( $this->node->ownerDocument );
+ }
+ return $this->xpath;
+ }
+
+ function __toString() {
+ if ( $this->node instanceof DOMNodeList ) {
+ $s = '';
+ foreach ( $this->node as $node ) {
+ $s .= $node->ownerDocument->saveXML( $node );
+ }
+ } else {
+ $s = $this->node->ownerDocument->saveXML( $this->node );
+ }
+ return $s;
+ }
+
+ function getChildren() {
+ return $this->node->childNodes ? new self( $this->node->childNodes ) : false;
+ }
+
+ function getFirstChild() {
+ return $this->node->firstChild ? new self( $this->node->firstChild ) : false;
+ }
+
+ function getNextSibling() {
+ return $this->node->nextSibling ? new self( $this->node->nextSibling ) : false;
+ }
+
+ function getChildrenOfType( $type ) {
+ return new self( $this->xpath->query( $type, $this->node ) );
+ }
+
+ function getLength() {
+ if ( $this->node instanceof DOMNodeList ) {
+ return $this->node->length;
+ } else {
+ return false;
+ }
+ }
+
+ function item( $i ) {
+ $item = $this->node->item( $i );
+ return $item ? new self( $item ) : false;
+ }
+
+ function getName() {
+ if ( $this->node instanceof DOMNodeList ) {
+ return '#nodelist';
+ } else {
+ return $this->node->nodeName;
+ }
+ }
+
+ /**
+ * Split a <part> node into an associative array containing:
+ * name PPNode name
+ * index String index
+ * value PPNode value
+ */
+ function splitArg() {
+ $names = $this->xpath->query( 'name', $this->node );
+ $values = $this->xpath->query( 'value', $this->node );
+ if ( !$names->length || !$values->length ) {
+ throw new MWException( 'Invalid brace node passed to ' . __METHOD__ );
+ }
+ $name = $names->item( 0 );
+ $index = $name->getAttribute( 'index' );
+ return array(
+ 'name' => new self( $name ),
+ 'index' => $index,
+ 'value' => new self( $values->item( 0 ) ) );
+ }
+
+ /**
+ * Split an <ext> node into an associative array containing name, attr, inner and close
+ * All values in the resulting array are PPNodes. Inner and close are optional.
+ */
+ function splitExt() {
+ $names = $this->xpath->query( 'name', $this->node );
+ $attrs = $this->xpath->query( 'attr', $this->node );
+ $inners = $this->xpath->query( 'inner', $this->node );
+ $closes = $this->xpath->query( 'close', $this->node );
+ if ( !$names->length || !$attrs->length ) {
+ throw new MWException( 'Invalid ext node passed to ' . __METHOD__ );
+ }
+ $parts = array(
+ 'name' => new self( $names->item( 0 ) ),
+ 'attr' => new self( $attrs->item( 0 ) ) );
+ if ( $inners->length ) {
+ $parts['inner'] = new self( $inners->item( 0 ) );
+ }
+ if ( $closes->length ) {
+ $parts['close'] = new self( $closes->item( 0 ) );
+ }
+ return $parts;
+ }
+
+ /**
+ * Split a <h> node
+ */
+ function splitHeading() {
+ if ( !$this->nodeName == 'h' ) {
+ throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
+ }
+ return array(
+ 'i' => $this->node->getAttribute( 'i' ),
+ 'level' => $this->node->getAttribute( 'level' ),
+ 'contents' => $this->getChildren()
+ );
+ }
+}
diff --git a/includes/parser/Preprocessor_Hash.php b/includes/parser/Preprocessor_Hash.php
new file mode 100644
index 00000000..b5775243
--- /dev/null
+++ b/includes/parser/Preprocessor_Hash.php
@@ -0,0 +1,1539 @@
+<?php
+
+/**
+ * Differences from DOM schema:
+ * * attribute nodes are children
+ * * <h> nodes that aren't at the top are replaced with <possible-h>
+ * @ingroup Parser
+ */
+class Preprocessor_Hash implements Preprocessor {
+ var $parser;
+
+ function __construct( $parser ) {
+ $this->parser = $parser;
+ }
+
+ function newFrame() {
+ return new PPFrame_Hash( $this );
+ }
+
+ function newCustomFrame( $args ) {
+ return new PPCustomFrame_Hash( $this, $args );
+ }
+
+ /**
+ * Preprocess some wikitext and return the document tree.
+ * This is the ghost of Parser::replace_variables().
+ *
+ * @param string $text The text to parse
+ * @param integer flags Bitwise combination of:
+ * Parser::PTD_FOR_INCLUSION Handle <noinclude>/<includeonly> as if the text is being
+ * included. Default is to assume a direct page view.
+ *
+ * The generated DOM tree must depend only on the input text and the flags.
+ * The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of bug 4899.
+ *
+ * Any flag added to the $flags parameter here, or any other parameter liable to cause a
+ * change in the DOM tree for a given text, must be passed through the section identifier
+ * in the section edit link and thus back to extractSections().
+ *
+ * The output of this function is currently only cached in process memory, but a persistent
+ * cache may be implemented at a later date which takes further advantage of these strict
+ * dependency requirements.
+ *
+ * @private
+ */
+ function preprocessToObj( $text, $flags = 0 ) {
+ wfProfileIn( __METHOD__ );
+
+ $rules = array(
+ '{' => array(
+ 'end' => '}',
+ 'names' => array(
+ 2 => 'template',
+ 3 => 'tplarg',
+ ),
+ 'min' => 2,
+ 'max' => 3,
+ ),
+ '[' => array(
+ 'end' => ']',
+ 'names' => array( 2 => null ),
+ 'min' => 2,
+ 'max' => 2,
+ )
+ );
+
+ $forInclusion = $flags & Parser::PTD_FOR_INCLUSION;
+
+ $xmlishElements = $this->parser->getStripList();
+ $enableOnlyinclude = false;
+ if ( $forInclusion ) {
+ $ignoredTags = array( 'includeonly', '/includeonly' );
+ $ignoredElements = array( 'noinclude' );
+ $xmlishElements[] = 'noinclude';
+ if ( strpos( $text, '<onlyinclude>' ) !== false && strpos( $text, '</onlyinclude>' ) !== false ) {
+ $enableOnlyinclude = true;
+ }
+ } else {
+ $ignoredTags = array( 'noinclude', '/noinclude', 'onlyinclude', '/onlyinclude' );
+ $ignoredElements = array( 'includeonly' );
+ $xmlishElements[] = 'includeonly';
+ }
+ $xmlishRegex = implode( '|', array_merge( $xmlishElements, $ignoredTags ) );
+
+ // Use "A" modifier (anchored) instead of "^", because ^ doesn't work with an offset
+ $elementsRegex = "~($xmlishRegex)(?:\s|\/>|>)|(!--)~iA";
+
+ $stack = new PPDStack_Hash;
+
+ $searchBase = "[{<\n";
+ $revText = strrev( $text ); // For fast reverse searches
+
+ $i = 0; # Input pointer, starts out pointing to a pseudo-newline before the start
+ $accum =& $stack->getAccum(); # Current accumulator
+ $findEquals = false; # True to find equals signs in arguments
+ $findPipe = false; # True to take notice of pipe characters
+ $headingIndex = 1;
+ $inHeading = false; # True if $i is inside a possible heading
+ $noMoreGT = false; # True if there are no more greater-than (>) signs right of $i
+ $findOnlyinclude = $enableOnlyinclude; # True to ignore all input up to the next <onlyinclude>
+ $fakeLineStart = true; # Do a line-start run without outputting an LF character
+
+ while ( true ) {
+ //$this->memCheck();
+
+ if ( $findOnlyinclude ) {
+ // Ignore all input up to the next <onlyinclude>
+ $startPos = strpos( $text, '<onlyinclude>', $i );
+ if ( $startPos === false ) {
+ // Ignored section runs to the end
+ $accum->addNodeWithText( 'ignore', substr( $text, $i ) );
+ break;
+ }
+ $tagEndPos = $startPos + strlen( '<onlyinclude>' ); // past-the-end
+ $accum->addNodeWithText( 'ignore', substr( $text, $i, $tagEndPos - $i ) );
+ $i = $tagEndPos;
+ $findOnlyinclude = false;
+ }
+
+ if ( $fakeLineStart ) {
+ $found = 'line-start';
+ $curChar = '';
+ } else {
+ # Find next opening brace, closing brace or pipe
+ $search = $searchBase;
+ if ( $stack->top === false ) {
+ $currentClosing = '';
+ } else {
+ $currentClosing = $stack->top->close;
+ $search .= $currentClosing;
+ }
+ if ( $findPipe ) {
+ $search .= '|';
+ }
+ if ( $findEquals ) {
+ // First equals will be for the template
+ $search .= '=';
+ }
+ $rule = null;
+ # Output literal section, advance input counter
+ $literalLength = strcspn( $text, $search, $i );
+ if ( $literalLength > 0 ) {
+ $accum->addLiteral( substr( $text, $i, $literalLength ) );
+ $i += $literalLength;
+ }
+ if ( $i >= strlen( $text ) ) {
+ if ( $currentClosing == "\n" ) {
+ // Do a past-the-end run to finish off the heading
+ $curChar = '';
+ $found = 'line-end';
+ } else {
+ # All done
+ break;
+ }
+ } else {
+ $curChar = $text[$i];
+ if ( $curChar == '|' ) {
+ $found = 'pipe';
+ } elseif ( $curChar == '=' ) {
+ $found = 'equals';
+ } elseif ( $curChar == '<' ) {
+ $found = 'angle';
+ } elseif ( $curChar == "\n" ) {
+ if ( $inHeading ) {
+ $found = 'line-end';
+ } else {
+ $found = 'line-start';
+ }
+ } elseif ( $curChar == $currentClosing ) {
+ $found = 'close';
+ } elseif ( isset( $rules[$curChar] ) ) {
+ $found = 'open';
+ $rule = $rules[$curChar];
+ } else {
+ # Some versions of PHP have a strcspn which stops on null characters
+ # Ignore and continue
+ ++$i;
+ continue;
+ }
+ }
+ }
+
+ if ( $found == 'angle' ) {
+ $matches = false;
+ // Handle </onlyinclude>
+ if ( $enableOnlyinclude && substr( $text, $i, strlen( '</onlyinclude>' ) ) == '</onlyinclude>' ) {
+ $findOnlyinclude = true;
+ continue;
+ }
+
+ // Determine element name
+ if ( !preg_match( $elementsRegex, $text, $matches, 0, $i + 1 ) ) {
+ // Element name missing or not listed
+ $accum->addLiteral( '<' );
+ ++$i;
+ continue;
+ }
+ // Handle comments
+ if ( isset( $matches[2] ) && $matches[2] == '!--' ) {
+ // To avoid leaving blank lines, when a comment is both preceded
+ // and followed by a newline (ignoring spaces), trim leading and
+ // trailing spaces and one of the newlines.
+
+ // Find the end
+ $endPos = strpos( $text, '-->', $i + 4 );
+ if ( $endPos === false ) {
+ // Unclosed comment in input, runs to end
+ $inner = substr( $text, $i );
+ $accum->addNodeWithText( 'comment', $inner );
+ $i = strlen( $text );
+ } else {
+ // Search backwards for leading whitespace
+ $wsStart = $i ? ( $i - strspn( $revText, ' ', strlen( $text ) - $i ) ) : 0;
+ // Search forwards for trailing whitespace
+ // $wsEnd will be the position of the last space
+ $wsEnd = $endPos + 2 + strspn( $text, ' ', $endPos + 3 );
+ // Eat the line if possible
+ // TODO: This could theoretically be done if $wsStart == 0, i.e. for comments at
+ // the overall start. That's not how Sanitizer::removeHTMLcomments() did it, but
+ // it's a possible beneficial b/c break.
+ if ( $wsStart > 0 && substr( $text, $wsStart - 1, 1 ) == "\n"
+ && substr( $text, $wsEnd + 1, 1 ) == "\n" )
+ {
+ $startPos = $wsStart;
+ $endPos = $wsEnd + 1;
+ // Remove leading whitespace from the end of the accumulator
+ // Sanity check first though
+ $wsLength = $i - $wsStart;
+ if ( $wsLength > 0
+ && $accum->lastNode instanceof PPNode_Hash_Text
+ && substr( $accum->lastNode->value, -$wsLength ) === str_repeat( ' ', $wsLength ) )
+ {
+ $accum->lastNode->value = substr( $accum->lastNode->value, 0, -$wsLength );
+ }
+ // Do a line-start run next time to look for headings after the comment
+ $fakeLineStart = true;
+ } else {
+ // No line to eat, just take the comment itself
+ $startPos = $i;
+ $endPos += 2;
+ }
+
+ if ( $stack->top ) {
+ $part = $stack->top->getCurrentPart();
+ if ( isset( $part->commentEnd ) && $part->commentEnd == $wsStart - 1 ) {
+ // Comments abutting, no change in visual end
+ $part->commentEnd = $wsEnd;
+ } else {
+ $part->visualEnd = $wsStart;
+ $part->commentEnd = $endPos;
+ }
+ }
+ $i = $endPos + 1;
+ $inner = substr( $text, $startPos, $endPos - $startPos + 1 );
+ $accum->addNodeWithText( 'comment', $inner );
+ }
+ continue;
+ }
+ $name = $matches[1];
+ $lowerName = strtolower( $name );
+ $attrStart = $i + strlen( $name ) + 1;
+
+ // Find end of tag
+ $tagEndPos = $noMoreGT ? false : strpos( $text, '>', $attrStart );
+ if ( $tagEndPos === false ) {
+ // Infinite backtrack
+ // Disable tag search to prevent worst-case O(N^2) performance
+ $noMoreGT = true;
+ $accum->addLiteral( '<' );
+ ++$i;
+ continue;
+ }
+
+ // Handle ignored tags
+ if ( in_array( $lowerName, $ignoredTags ) ) {
+ $accum->addNodeWithText( 'ignore', substr( $text, $i, $tagEndPos - $i + 1 ) );
+ $i = $tagEndPos + 1;
+ continue;
+ }
+
+ $tagStartPos = $i;
+ if ( $text[$tagEndPos-1] == '/' ) {
+ // Short end tag
+ $attrEnd = $tagEndPos - 1;
+ $inner = null;
+ $i = $tagEndPos + 1;
+ $close = null;
+ } else {
+ $attrEnd = $tagEndPos;
+ // Find closing tag
+ if ( preg_match( "/<\/$name\s*>/i", $text, $matches, PREG_OFFSET_CAPTURE, $tagEndPos + 1 ) ) {
+ $inner = substr( $text, $tagEndPos + 1, $matches[0][1] - $tagEndPos - 1 );
+ $i = $matches[0][1] + strlen( $matches[0][0] );
+ $close = $matches[0][0];
+ } else {
+ // No end tag -- let it run out to the end of the text.
+ $inner = substr( $text, $tagEndPos + 1 );
+ $i = strlen( $text );
+ $close = null;
+ }
+ }
+ // <includeonly> and <noinclude> just become <ignore> tags
+ if ( in_array( $lowerName, $ignoredElements ) ) {
+ $accum->addNodeWithText( 'ignore', substr( $text, $tagStartPos, $i - $tagStartPos ) );
+ continue;
+ }
+
+ if ( $attrEnd <= $attrStart ) {
+ $attr = '';
+ } else {
+ // Note that the attr element contains the whitespace between name and attribute,
+ // this is necessary for precise reconstruction during pre-save transform.
+ $attr = substr( $text, $attrStart, $attrEnd - $attrStart );
+ }
+
+ $extNode = new PPNode_Hash_Tree( 'ext' );
+ $extNode->addChild( PPNode_Hash_Tree::newWithText( 'name', $name ) );
+ $extNode->addChild( PPNode_Hash_Tree::newWithText( 'attr', $attr ) );
+ if ( $inner !== null ) {
+ $extNode->addChild( PPNode_Hash_Tree::newWithText( 'inner', $inner ) );
+ }
+ if ( $close !== null ) {
+ $extNode->addChild( PPNode_Hash_Tree::newWithText( 'close', $close ) );
+ }
+ $accum->addNode( $extNode );
+ }
+
+ elseif ( $found == 'line-start' ) {
+ // Is this the start of a heading?
+ // Line break belongs before the heading element in any case
+ if ( $fakeLineStart ) {
+ $fakeLineStart = false;
+ } else {
+ $accum->addLiteral( $curChar );
+ $i++;
+ }
+
+ $count = strspn( $text, '=', $i, 6 );
+ if ( $count == 1 && $findEquals ) {
+ // DWIM: This looks kind of like a name/value separator
+ // Let's let the equals handler have it and break the potential heading
+ // This is heuristic, but AFAICT the methods for completely correct disambiguation are very complex.
+ } elseif ( $count > 0 ) {
+ $piece = array(
+ 'open' => "\n",
+ 'close' => "\n",
+ 'parts' => array( new PPDPart_Hash( str_repeat( '=', $count ) ) ),
+ 'startPos' => $i,
+ 'count' => $count );
+ $stack->push( $piece );
+ $accum =& $stack->getAccum();
+ extract( $stack->getFlags() );
+ $i += $count;
+ }
+ }
+
+ elseif ( $found == 'line-end' ) {
+ $piece = $stack->top;
+ // A heading must be open, otherwise \n wouldn't have been in the search list
+ assert( $piece->open == "\n" );
+ $part = $piece->getCurrentPart();
+ // Search back through the input to see if it has a proper close
+ // Do this using the reversed string since the other solutions (end anchor, etc.) are inefficient
+ $wsLength = strspn( $revText, " \t", strlen( $text ) - $i );
+ $searchStart = $i - $wsLength;
+ if ( isset( $part->commentEnd ) && $searchStart - 1 == $part->commentEnd ) {
+ // Comment found at line end
+ // Search for equals signs before the comment
+ $searchStart = $part->visualEnd;
+ $searchStart -= strspn( $revText, " \t", strlen( $text ) - $searchStart );
+ }
+ $count = $piece->count;
+ $equalsLength = strspn( $revText, '=', strlen( $text ) - $searchStart );
+ if ( $equalsLength > 0 ) {
+ if ( $i - $equalsLength == $piece->startPos ) {
+ // This is just a single string of equals signs on its own line
+ // Replicate the doHeadings behaviour /={count}(.+)={count}/
+ // First find out how many equals signs there really are (don't stop at 6)
+ $count = $equalsLength;
+ if ( $count < 3 ) {
+ $count = 0;
+ } else {
+ $count = min( 6, intval( ( $count - 1 ) / 2 ) );
+ }
+ } else {
+ $count = min( $equalsLength, $count );
+ }
+ if ( $count > 0 ) {
+ // Normal match, output <h>
+ $element = new PPNode_Hash_Tree( 'possible-h' );
+ $element->addChild( new PPNode_Hash_Attr( 'level', $count ) );
+ $element->addChild( new PPNode_Hash_Attr( 'i', $headingIndex++ ) );
+ $element->lastChild->nextSibling = $accum->firstNode;
+ $element->lastChild = $accum->lastNode;
+ } else {
+ // Single equals sign on its own line, count=0
+ $element = $accum;
+ }
+ } else {
+ // No match, no <h>, just pass down the inner text
+ $element = $accum;
+ }
+ // Unwind the stack
+ $stack->pop();
+ $accum =& $stack->getAccum();
+ extract( $stack->getFlags() );
+
+ // Append the result to the enclosing accumulator
+ if ( $element instanceof PPNode ) {
+ $accum->addNode( $element );
+ } else {
+ $accum->addAccum( $element );
+ }
+ // Note that we do NOT increment the input pointer.
+ // This is because the closing linebreak could be the opening linebreak of
+ // another heading. Infinite loops are avoided because the next iteration MUST
+ // hit the heading open case above, which unconditionally increments the
+ // input pointer.
+ }
+
+ elseif ( $found == 'open' ) {
+ # count opening brace characters
+ $count = strspn( $text, $curChar, $i );
+
+ # we need to add to stack only if opening brace count is enough for one of the rules
+ if ( $count >= $rule['min'] ) {
+ # Add it to the stack
+ $piece = array(
+ 'open' => $curChar,
+ 'close' => $rule['end'],
+ 'count' => $count,
+ 'lineStart' => ($i > 0 && $text[$i-1] == "\n"),
+ );
+
+ $stack->push( $piece );
+ $accum =& $stack->getAccum();
+ extract( $stack->getFlags() );
+ } else {
+ # Add literal brace(s)
+ $accum->addLiteral( str_repeat( $curChar, $count ) );
+ }
+ $i += $count;
+ }
+
+ elseif ( $found == 'close' ) {
+ $piece = $stack->top;
+ # lets check if there are enough characters for closing brace
+ $maxCount = $piece->count;
+ $count = strspn( $text, $curChar, $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;
+ $rule = $rules[$piece->open];
+ if ( $count > $rule['max'] ) {
+ # The specified maximum exists in the callback array, unless the caller
+ # has made an error
+ $matchingCount = $rule['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, $rule['names'] ) ) {
+ --$matchingCount;
+ }
+ }
+
+ if ($matchingCount <= 0) {
+ # No matching element found in callback array
+ # Output a literal closing brace and continue
+ $accum->addLiteral( str_repeat( $curChar, $count ) );
+ $i += $count;
+ continue;
+ }
+ $name = $rule['names'][$matchingCount];
+ if ( $name === null ) {
+ // No element, just literal text
+ $element = $piece->breakSyntax( $matchingCount );
+ $element->addLiteral( str_repeat( $rule['end'], $matchingCount ) );
+ } else {
+ # Create XML element
+ # Note: $parts is already XML, does not need to be encoded further
+ $parts = $piece->parts;
+ $titleAccum = $parts[0]->out;
+ unset( $parts[0] );
+
+ $element = new PPNode_Hash_Tree( $name );
+
+ # The invocation is at the start of the line if lineStart is set in
+ # the stack, and all opening brackets are used up.
+ if ( $maxCount == $matchingCount && !empty( $piece->lineStart ) ) {
+ $element->addChild( new PPNode_Hash_Attr( 'lineStart', 1 ) );
+ }
+ $titleNode = new PPNode_Hash_Tree( 'title' );
+ $titleNode->firstChild = $titleAccum->firstNode;
+ $titleNode->lastChild = $titleAccum->lastNode;
+ $element->addChild( $titleNode );
+ $argIndex = 1;
+ foreach ( $parts as $partIndex => $part ) {
+ if ( isset( $part->eqpos ) ) {
+ // Find equals
+ $lastNode = false;
+ for ( $node = $part->out->firstNode; $node; $node = $node->nextSibling ) {
+ if ( $node === $part->eqpos ) {
+ break;
+ }
+ $lastNode = $node;
+ }
+ if ( !$node ) {
+ throw new MWException( __METHOD__. ': eqpos not found' );
+ }
+ if ( $node->name !== 'equals' ) {
+ throw new MWException( __METHOD__ .': eqpos is not equals' );
+ }
+ $equalsNode = $node;
+
+ // Construct name node
+ $nameNode = new PPNode_Hash_Tree( 'name' );
+ if ( $lastNode !== false ) {
+ $lastNode->nextSibling = false;
+ $nameNode->firstChild = $part->out->firstNode;
+ $nameNode->lastChild = $lastNode;
+ }
+
+ // Construct value node
+ $valueNode = new PPNode_Hash_Tree( 'value' );
+ if ( $equalsNode->nextSibling !== false ) {
+ $valueNode->firstChild = $equalsNode->nextSibling;
+ $valueNode->lastChild = $part->out->lastNode;
+ }
+ $partNode = new PPNode_Hash_Tree( 'part' );
+ $partNode->addChild( $nameNode );
+ $partNode->addChild( $equalsNode->firstChild );
+ $partNode->addChild( $valueNode );
+ $element->addChild( $partNode );
+ } else {
+ $partNode = new PPNode_Hash_Tree( 'part' );
+ $nameNode = new PPNode_Hash_Tree( 'name' );
+ $nameNode->addChild( new PPNode_Hash_Attr( 'index', $argIndex++ ) );
+ $valueNode = new PPNode_Hash_Tree( 'value' );
+ $valueNode->firstChild = $part->out->firstNode;
+ $valueNode->lastChild = $part->out->lastNode;
+ $partNode->addChild( $nameNode );
+ $partNode->addChild( $valueNode );
+ $element->addChild( $partNode );
+ }
+ }
+ }
+
+ # Advance input pointer
+ $i += $matchingCount;
+
+ # Unwind the stack
+ $stack->pop();
+ $accum =& $stack->getAccum();
+
+ # Re-add the old stack element if it still has unmatched opening characters remaining
+ if ($matchingCount < $piece->count) {
+ $piece->parts = array( new PPDPart_Hash );
+ $piece->count -= $matchingCount;
+ # do we still qualify for any callback with remaining count?
+ $names = $rules[$piece->open]['names'];
+ $skippedBraces = 0;
+ $enclosingAccum =& $accum;
+ while ( $piece->count ) {
+ if ( array_key_exists( $piece->count, $names ) ) {
+ $stack->push( $piece );
+ $accum =& $stack->getAccum();
+ break;
+ }
+ --$piece->count;
+ $skippedBraces ++;
+ }
+ $enclosingAccum->addLiteral( str_repeat( $piece->open, $skippedBraces ) );
+ }
+
+ extract( $stack->getFlags() );
+
+ # Add XML element to the enclosing accumulator
+ if ( $element instanceof PPNode ) {
+ $accum->addNode( $element );
+ } else {
+ $accum->addAccum( $element );
+ }
+ }
+
+ elseif ( $found == 'pipe' ) {
+ $findEquals = true; // shortcut for getFlags()
+ $stack->addPart();
+ $accum =& $stack->getAccum();
+ ++$i;
+ }
+
+ elseif ( $found == 'equals' ) {
+ $findEquals = false; // shortcut for getFlags()
+ $accum->addNodeWithText( 'equals', '=' );
+ $stack->getCurrentPart()->eqpos = $accum->lastNode;
+ ++$i;
+ }
+ }
+
+ # Output any remaining unclosed brackets
+ foreach ( $stack->stack as $piece ) {
+ $stack->rootAccum->addAccum( $piece->breakSyntax() );
+ }
+
+ # Enable top-level headings
+ for ( $node = $stack->rootAccum->firstNode; $node; $node = $node->nextSibling ) {
+ if ( isset( $node->name ) && $node->name === 'possible-h' ) {
+ $node->name = 'h';
+ }
+ }
+
+ $rootNode = new PPNode_Hash_Tree( 'root' );
+ $rootNode->firstChild = $stack->rootAccum->firstNode;
+ $rootNode->lastChild = $stack->rootAccum->lastNode;
+ wfProfileOut( __METHOD__ );
+ return $rootNode;
+ }
+}
+
+/**
+ * Stack class to help Preprocessor::preprocessToObj()
+ * @ingroup Parser
+ */
+class PPDStack_Hash extends PPDStack {
+ function __construct() {
+ $this->elementClass = 'PPDStackElement_Hash';
+ parent::__construct();
+ $this->rootAccum = new PPDAccum_Hash;
+ }
+}
+
+/**
+ * @ingroup Parser
+ */
+class PPDStackElement_Hash extends PPDStackElement {
+ function __construct( $data = array() ) {
+ $this->partClass = 'PPDPart_Hash';
+ parent::__construct( $data );
+ }
+
+ /**
+ * Get the accumulator that would result if the close is not found.
+ */
+ function breakSyntax( $openingCount = false ) {
+ if ( $this->open == "\n" ) {
+ $accum = $this->parts[0]->out;
+ } else {
+ if ( $openingCount === false ) {
+ $openingCount = $this->count;
+ }
+ $accum = new PPDAccum_Hash;
+ $accum->addLiteral( str_repeat( $this->open, $openingCount ) );
+ $first = true;
+ foreach ( $this->parts as $part ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $accum->addLiteral( '|' );
+ }
+ $accum->addAccum( $part->out );
+ }
+ }
+ return $accum;
+ }
+}
+
+/**
+ * @ingroup Parser
+ */
+class PPDPart_Hash extends PPDPart {
+ function __construct( $out = '' ) {
+ $accum = new PPDAccum_Hash;
+ if ( $out !== '' ) {
+ $accum->addLiteral( $out );
+ }
+ parent::__construct( $accum );
+ }
+}
+
+/**
+ * @ingroup Parser
+ */
+class PPDAccum_Hash {
+ var $firstNode, $lastNode;
+
+ function __construct() {
+ $this->firstNode = $this->lastNode = false;
+ }
+
+ /**
+ * Append a string literal
+ */
+ function addLiteral( $s ) {
+ if ( $this->lastNode === false ) {
+ $this->firstNode = $this->lastNode = new PPNode_Hash_Text( $s );
+ } elseif ( $this->lastNode instanceof PPNode_Hash_Text ) {
+ $this->lastNode->value .= $s;
+ } else {
+ $this->lastNode->nextSibling = new PPNode_Hash_Text( $s );
+ $this->lastNode = $this->lastNode->nextSibling;
+ }
+ }
+
+ /**
+ * Append a PPNode
+ */
+ function addNode( PPNode $node ) {
+ if ( $this->lastNode === false ) {
+ $this->firstNode = $this->lastNode = $node;
+ } else {
+ $this->lastNode->nextSibling = $node;
+ $this->lastNode = $node;
+ }
+ }
+
+ /**
+ * Append a tree node with text contents
+ */
+ function addNodeWithText( $name, $value ) {
+ $node = PPNode_Hash_Tree::newWithText( $name, $value );
+ $this->addNode( $node );
+ }
+
+ /**
+ * Append a PPAccum_Hash
+ * Takes over ownership of the nodes in the source argument. These nodes may
+ * subsequently be modified, especially nextSibling.
+ */
+ function addAccum( $accum ) {
+ if ( $accum->lastNode === false ) {
+ // nothing to add
+ } elseif ( $this->lastNode === false ) {
+ $this->firstNode = $accum->firstNode;
+ $this->lastNode = $accum->lastNode;
+ } else {
+ $this->lastNode->nextSibling = $accum->firstNode;
+ $this->lastNode = $accum->lastNode;
+ }
+ }
+}
+
+/**
+ * An expansion frame, used as a context to expand the result of preprocessToObj()
+ * @ingroup Parser
+ */
+class PPFrame_Hash implements PPFrame {
+ var $preprocessor, $parser, $title;
+ var $titleCache;
+
+ /**
+ * Hashtable listing templates which are disallowed for expansion in this frame,
+ * having been encountered previously in parent frames.
+ */
+ var $loopCheckHash;
+
+ /**
+ * Recursion depth of this frame, top = 0
+ */
+ var $depth;
+
+
+ /**
+ * Construct a new preprocessor frame.
+ * @param Preprocessor $preprocessor The parent preprocessor
+ */
+ function __construct( $preprocessor ) {
+ $this->preprocessor = $preprocessor;
+ $this->parser = $preprocessor->parser;
+ $this->title = $this->parser->mTitle;
+ $this->titleCache = array( $this->title ? $this->title->getPrefixedDBkey() : false );
+ $this->loopCheckHash = array();
+ $this->depth = 0;
+ }
+
+ /**
+ * Create a new child frame
+ * $args is optionally a multi-root PPNode or array containing the template arguments
+ */
+ function newChild( $args = false, $title = false ) {
+ $namedArgs = array();
+ $numberedArgs = array();
+ if ( $title === false ) {
+ $title = $this->title;
+ }
+ if ( $args !== false ) {
+ $xpath = false;
+ if ( $args instanceof PPNode_Hash_Array ) {
+ $args = $args->value;
+ } elseif ( !is_array( $args ) ) {
+ throw new MWException( __METHOD__ . ': $args must be array or PPNode_Hash_Array' );
+ }
+ foreach ( $args as $arg ) {
+ $bits = $arg->splitArg();
+ if ( $bits['index'] !== '' ) {
+ // Numbered parameter
+ $numberedArgs[$bits['index']] = $bits['value'];
+ unset( $namedArgs[$bits['index']] );
+ } else {
+ // Named parameter
+ $name = trim( $this->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
+ $namedArgs[$name] = $bits['value'];
+ unset( $numberedArgs[$name] );
+ }
+ }
+ }
+ return new PPTemplateFrame_Hash( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title );
+ }
+
+ function expand( $root, $flags = 0 ) {
+ if ( is_string( $root ) ) {
+ return $root;
+ }
+
+ if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->mMaxPPNodeCount )
+ {
+ return '<span class="error">Node-count limit exceeded</span>';
+ }
+ if ( $this->depth > $this->parser->mOptions->mMaxPPExpandDepth ) {
+ return '<span class="error">Expansion depth limit exceeded</span>';
+ }
+ ++$this->depth;
+
+ $outStack = array( '', '' );
+ $iteratorStack = array( false, $root );
+ $indexStack = array( 0, 0 );
+
+ while ( count( $iteratorStack ) > 1 ) {
+ $level = count( $outStack ) - 1;
+ $iteratorNode =& $iteratorStack[ $level ];
+ $out =& $outStack[$level];
+ $index =& $indexStack[$level];
+
+ if ( is_array( $iteratorNode ) ) {
+ if ( $index >= count( $iteratorNode ) ) {
+ // All done with this iterator
+ $iteratorStack[$level] = false;
+ $contextNode = false;
+ } else {
+ $contextNode = $iteratorNode[$index];
+ $index++;
+ }
+ } elseif ( $iteratorNode instanceof PPNode_Hash_Array ) {
+ if ( $index >= $iteratorNode->getLength() ) {
+ // All done with this iterator
+ $iteratorStack[$level] = false;
+ $contextNode = false;
+ } else {
+ $contextNode = $iteratorNode->item( $index );
+ $index++;
+ }
+ } else {
+ // Copy to $contextNode and then delete from iterator stack,
+ // because this is not an iterator but we do have to execute it once
+ $contextNode = $iteratorStack[$level];
+ $iteratorStack[$level] = false;
+ }
+
+ $newIterator = false;
+
+ if ( $contextNode === false ) {
+ // nothing to do
+ } elseif ( is_string( $contextNode ) ) {
+ $out .= $contextNode;
+ } elseif ( is_array( $contextNode ) || $contextNode instanceof PPNode_Hash_Array ) {
+ $newIterator = $contextNode;
+ } elseif ( $contextNode instanceof PPNode_Hash_Attr ) {
+ // No output
+ } elseif ( $contextNode instanceof PPNode_Hash_Text ) {
+ $out .= $contextNode->value;
+ } elseif ( $contextNode instanceof PPNode_Hash_Tree ) {
+ if ( $contextNode->name == 'template' ) {
+ # Double-brace expansion
+ $bits = $contextNode->splitTemplate();
+ if ( $flags & self::NO_TEMPLATES ) {
+ $newIterator = $this->virtualBracketedImplode( '{{', '|', '}}', $bits['title'], $bits['parts'] );
+ } else {
+ $ret = $this->parser->braceSubstitution( $bits, $this );
+ if ( isset( $ret['object'] ) ) {
+ $newIterator = $ret['object'];
+ } else {
+ $out .= $ret['text'];
+ }
+ }
+ } elseif ( $contextNode->name == 'tplarg' ) {
+ # Triple-brace expansion
+ $bits = $contextNode->splitTemplate();
+ if ( $flags & self::NO_ARGS ) {
+ $newIterator = $this->virtualBracketedImplode( '{{{', '|', '}}}', $bits['title'], $bits['parts'] );
+ } else {
+ $ret = $this->parser->argSubstitution( $bits, $this );
+ if ( isset( $ret['object'] ) ) {
+ $newIterator = $ret['object'];
+ } else {
+ $out .= $ret['text'];
+ }
+ }
+ } elseif ( $contextNode->name == 'comment' ) {
+ # HTML-style comment
+ # Remove it in HTML, pre+remove and STRIP_COMMENTS modes
+ if ( $this->parser->ot['html']
+ || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() )
+ || ( $flags & self::STRIP_COMMENTS ) )
+ {
+ $out .= '';
+ }
+ # Add a strip marker in PST mode so that pstPass2() can run some old-fashioned regexes on the result
+ # Not in RECOVER_COMMENTS mode (extractSections) though
+ elseif ( $this->parser->ot['wiki'] && ! ( $flags & self::RECOVER_COMMENTS ) ) {
+ $out .= $this->parser->insertStripItem( $contextNode->firstChild->value );
+ }
+ # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove
+ else {
+ $out .= $contextNode->firstChild->value;
+ }
+ } elseif ( $contextNode->name == 'ignore' ) {
+ # Output suppression used by <includeonly> etc.
+ # OT_WIKI will only respect <ignore> in substed templates.
+ # The other output types respect it unless NO_IGNORE is set.
+ # extractSections() sets NO_IGNORE and so never respects it.
+ if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] ) || ( $flags & self::NO_IGNORE ) ) {
+ $out .= $contextNode->firstChild->value;
+ } else {
+ //$out .= '';
+ }
+ } elseif ( $contextNode->name == 'ext' ) {
+ # Extension tag
+ $bits = $contextNode->splitExt() + array( 'attr' => null, 'inner' => null, 'close' => null );
+ $out .= $this->parser->extensionSubstitution( $bits, $this );
+ } elseif ( $contextNode->name == 'h' ) {
+ # Heading
+ if ( $this->parser->ot['html'] ) {
+ # Expand immediately and insert heading index marker
+ $s = '';
+ for ( $node = $contextNode->firstChild; $node; $node = $node->nextSibling ) {
+ $s .= $this->expand( $node, $flags );
+ }
+
+ $bits = $contextNode->splitHeading();
+ $titleText = $this->title->getPrefixedDBkey();
+ $this->parser->mHeadings[] = array( $titleText, $bits['i'] );
+ $serial = count( $this->parser->mHeadings ) - 1;
+ $marker = "{$this->parser->mUniqPrefix}-h-$serial-" . Parser::MARKER_SUFFIX;
+ $s = substr( $s, 0, $bits['level'] ) . $marker . substr( $s, $bits['level'] );
+ $this->parser->mStripState->general->setPair( $marker, '' );
+ $out .= $s;
+ } else {
+ # Expand in virtual stack
+ $newIterator = $contextNode->getChildren();
+ }
+ } else {
+ # Generic recursive expansion
+ $newIterator = $contextNode->getChildren();
+ }
+ } else {
+ throw new MWException( __METHOD__.': Invalid parameter type' );
+ }
+
+ if ( $newIterator !== false ) {
+ $outStack[] = '';
+ $iteratorStack[] = $newIterator;
+ $indexStack[] = 0;
+ } elseif ( $iteratorStack[$level] === false ) {
+ // Return accumulated value to parent
+ // With tail recursion
+ while ( $iteratorStack[$level] === false && $level > 0 ) {
+ $outStack[$level - 1] .= $out;
+ array_pop( $outStack );
+ array_pop( $iteratorStack );
+ array_pop( $indexStack );
+ $level--;
+ }
+ }
+ }
+ --$this->depth;
+ return $outStack[0];
+ }
+
+ function implodeWithFlags( $sep, $flags /*, ... */ ) {
+ $args = array_slice( func_get_args(), 2 );
+
+ $first = true;
+ $s = '';
+ foreach ( $args as $root ) {
+ if ( $root instanceof PPNode_Hash_Array ) {
+ $root = $root->value;
+ }
+ if ( !is_array( $root ) ) {
+ $root = array( $root );
+ }
+ foreach ( $root as $node ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $s .= $sep;
+ }
+ $s .= $this->expand( $node, $flags );
+ }
+ }
+ return $s;
+ }
+
+ /**
+ * Implode with no flags specified
+ * This previously called implodeWithFlags but has now been inlined to reduce stack depth
+ */
+ function implode( $sep /*, ... */ ) {
+ $args = array_slice( func_get_args(), 1 );
+
+ $first = true;
+ $s = '';
+ foreach ( $args as $root ) {
+ if ( $root instanceof PPNode_Hash_Array ) {
+ $root = $root->value;
+ }
+ if ( !is_array( $root ) ) {
+ $root = array( $root );
+ }
+ foreach ( $root as $node ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $s .= $sep;
+ }
+ $s .= $this->expand( $node );
+ }
+ }
+ return $s;
+ }
+
+ /**
+ * Makes an object that, when expand()ed, will be the same as one obtained
+ * with implode()
+ */
+ function virtualImplode( $sep /*, ... */ ) {
+ $args = array_slice( func_get_args(), 1 );
+ $out = array();
+ $first = true;
+
+ foreach ( $args as $root ) {
+ if ( $root instanceof PPNode_Hash_Array ) {
+ $root = $root->value;
+ }
+ if ( !is_array( $root ) ) {
+ $root = array( $root );
+ }
+ foreach ( $root as $node ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $out[] = $sep;
+ }
+ $out[] = $node;
+ }
+ }
+ return new PPNode_Hash_Array( $out );
+ }
+
+ /**
+ * Virtual implode with brackets
+ */
+ function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) {
+ $args = array_slice( func_get_args(), 3 );
+ $out = array( $start );
+ $first = true;
+
+ foreach ( $args as $root ) {
+ if ( $root instanceof PPNode_Hash_Array ) {
+ $root = $root->value;
+ }
+ if ( !is_array( $root ) ) {
+ $root = array( $root );
+ }
+ foreach ( $root as $node ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $out[] = $sep;
+ }
+ $out[] = $node;
+ }
+ }
+ $out[] = $end;
+ return new PPNode_Hash_Array( $out );
+ }
+
+ function __toString() {
+ return 'frame{}';
+ }
+
+ function getPDBK( $level = false ) {
+ if ( $level === false ) {
+ return $this->title->getPrefixedDBkey();
+ } else {
+ return isset( $this->titleCache[$level] ) ? $this->titleCache[$level] : false;
+ }
+ }
+
+ /**
+ * Returns true if there are no arguments in this frame
+ */
+ function isEmpty() {
+ return true;
+ }
+
+ function getArgument( $name ) {
+ return false;
+ }
+
+ /**
+ * Returns true if the infinite loop check is OK, false if a loop is detected
+ */
+ function loopCheck( $title ) {
+ return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] );
+ }
+
+ /**
+ * Return true if the frame is a template frame
+ */
+ function isTemplate() {
+ return false;
+ }
+}
+
+/**
+ * Expansion frame with template arguments
+ * @ingroup Parser
+ */
+class PPTemplateFrame_Hash extends PPFrame_Hash {
+ var $numberedArgs, $namedArgs, $parent;
+ var $numberedExpansionCache, $namedExpansionCache;
+
+ function __construct( $preprocessor, $parent = false, $numberedArgs = array(), $namedArgs = array(), $title = false ) {
+ $this->preprocessor = $preprocessor;
+ $this->parser = $preprocessor->parser;
+ $this->parent = $parent;
+ $this->numberedArgs = $numberedArgs;
+ $this->namedArgs = $namedArgs;
+ $this->title = $title;
+ $pdbk = $title ? $title->getPrefixedDBkey() : false;
+ $this->titleCache = $parent->titleCache;
+ $this->titleCache[] = $pdbk;
+ $this->loopCheckHash = /*clone*/ $parent->loopCheckHash;
+ if ( $pdbk !== false ) {
+ $this->loopCheckHash[$pdbk] = true;
+ }
+ $this->depth = $parent->depth + 1;
+ $this->numberedExpansionCache = $this->namedExpansionCache = array();
+ }
+
+ function __toString() {
+ $s = 'tplframe{';
+ $first = true;
+ $args = $this->numberedArgs + $this->namedArgs;
+ foreach ( $args as $name => $value ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $s .= ', ';
+ }
+ $s .= "\"$name\":\"" .
+ str_replace( '"', '\\"', $value->__toString() ) . '"';
+ }
+ $s .= '}';
+ return $s;
+ }
+ /**
+ * Returns true if there are no arguments in this frame
+ */
+ function isEmpty() {
+ return !count( $this->numberedArgs ) && !count( $this->namedArgs );
+ }
+
+ function getNumberedArgument( $index ) {
+ if ( !isset( $this->numberedArgs[$index] ) ) {
+ return false;
+ }
+ if ( !isset( $this->numberedExpansionCache[$index] ) ) {
+ # No trimming for unnamed arguments
+ $this->numberedExpansionCache[$index] = $this->parent->expand( $this->numberedArgs[$index], self::STRIP_COMMENTS );
+ }
+ return $this->numberedExpansionCache[$index];
+ }
+
+ function getNamedArgument( $name ) {
+ if ( !isset( $this->namedArgs[$name] ) ) {
+ return false;
+ }
+ if ( !isset( $this->namedExpansionCache[$name] ) ) {
+ # Trim named arguments post-expand, for backwards compatibility
+ $this->namedExpansionCache[$name] = trim(
+ $this->parent->expand( $this->namedArgs[$name], self::STRIP_COMMENTS ) );
+ }
+ return $this->namedExpansionCache[$name];
+ }
+
+ function getArgument( $name ) {
+ $text = $this->getNumberedArgument( $name );
+ if ( $text === false ) {
+ $text = $this->getNamedArgument( $name );
+ }
+ return $text;
+ }
+
+ /**
+ * Return true if the frame is a template frame
+ */
+ function isTemplate() {
+ return true;
+ }
+}
+
+/**
+ * Expansion frame with custom arguments
+ * @ingroup Parser
+ */
+class PPCustomFrame_Hash extends PPFrame_Hash {
+ var $args;
+
+ function __construct( $preprocessor, $args ) {
+ $this->preprocessor = $preprocessor;
+ $this->parser = $preprocessor->parser;
+ $this->args = $args;
+ }
+
+ function __toString() {
+ $s = 'cstmframe{';
+ $first = true;
+ foreach ( $this->args as $name => $value ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $s .= ', ';
+ }
+ $s .= "\"$name\":\"" .
+ str_replace( '"', '\\"', $value->__toString() ) . '"';
+ }
+ $s .= '}';
+ return $s;
+ }
+
+ function isEmpty() {
+ return !count( $this->args );
+ }
+
+ function getArgument( $index ) {
+ return $this->args[$index];
+ }
+}
+
+/**
+ * @ingroup Parser
+ */
+class PPNode_Hash_Tree implements PPNode {
+ var $name, $firstChild, $lastChild, $nextSibling;
+
+ function __construct( $name ) {
+ $this->name = $name;
+ $this->firstChild = $this->lastChild = $this->nextSibling = false;
+ }
+
+ function __toString() {
+ $inner = '';
+ $attribs = '';
+ for ( $node = $this->firstChild; $node; $node = $node->nextSibling ) {
+ if ( $node instanceof PPNode_Hash_Attr ) {
+ $attribs .= ' ' . $node->name . '="' . htmlspecialchars( $node->value ) . '"';
+ } else {
+ $inner .= $node->__toString();
+ }
+ }
+ if ( $inner === '' ) {
+ return "<{$this->name}$attribs/>";
+ } else {
+ return "<{$this->name}$attribs>$inner</{$this->name}>";
+ }
+ }
+
+ static function newWithText( $name, $text ) {
+ $obj = new self( $name );
+ $obj->addChild( new PPNode_Hash_Text( $text ) );
+ return $obj;
+ }
+
+ function addChild( $node ) {
+ if ( $this->lastChild === false ) {
+ $this->firstChild = $this->lastChild = $node;
+ } else {
+ $this->lastChild->nextSibling = $node;
+ $this->lastChild = $node;
+ }
+ }
+
+ function getChildren() {
+ $children = array();
+ for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
+ $children[] = $child;
+ }
+ return new PPNode_Hash_Array( $children );
+ }
+
+ function getFirstChild() {
+ return $this->firstChild;
+ }
+
+ function getNextSibling() {
+ return $this->nextSibling;
+ }
+
+ function getChildrenOfType( $name ) {
+ $children = array();
+ for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
+ if ( isset( $child->name ) && $child->name === $name ) {
+ $children[] = $name;
+ }
+ }
+ return $children;
+ }
+
+ function getLength() { return false; }
+ function item( $i ) { return false; }
+
+ function getName() {
+ return $this->name;
+ }
+
+ /**
+ * Split a <part> node into an associative array containing:
+ * name PPNode name
+ * index String index
+ * value PPNode value
+ */
+ function splitArg() {
+ $bits = array();
+ for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
+ if ( !isset( $child->name ) ) {
+ continue;
+ }
+ if ( $child->name === 'name' ) {
+ $bits['name'] = $child;
+ if ( $child->firstChild instanceof PPNode_Hash_Attr
+ && $child->firstChild->name === 'index' )
+ {
+ $bits['index'] = $child->firstChild->value;
+ }
+ } elseif ( $child->name === 'value' ) {
+ $bits['value'] = $child;
+ }
+ }
+
+ if ( !isset( $bits['name'] ) ) {
+ throw new MWException( 'Invalid brace node passed to ' . __METHOD__ );
+ }
+ if ( !isset( $bits['index'] ) ) {
+ $bits['index'] = '';
+ }
+ return $bits;
+ }
+
+ /**
+ * Split an <ext> node into an associative array containing name, attr, inner and close
+ * All values in the resulting array are PPNodes. Inner and close are optional.
+ */
+ function splitExt() {
+ $bits = array();
+ for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
+ if ( !isset( $child->name ) ) {
+ continue;
+ }
+ if ( $child->name == 'name' ) {
+ $bits['name'] = $child;
+ } elseif ( $child->name == 'attr' ) {
+ $bits['attr'] = $child;
+ } elseif ( $child->name == 'inner' ) {
+ $bits['inner'] = $child;
+ } elseif ( $child->name == 'close' ) {
+ $bits['close'] = $child;
+ }
+ }
+ if ( !isset( $bits['name'] ) ) {
+ throw new MWException( 'Invalid ext node passed to ' . __METHOD__ );
+ }
+ return $bits;
+ }
+
+ /**
+ * Split an <h> node
+ */
+ function splitHeading() {
+ if ( $this->name !== 'h' ) {
+ throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
+ }
+ $bits = array();
+ for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
+ if ( !isset( $child->name ) ) {
+ continue;
+ }
+ if ( $child->name == 'i' ) {
+ $bits['i'] = $child->value;
+ } elseif ( $child->name == 'level' ) {
+ $bits['level'] = $child->value;
+ }
+ }
+ if ( !isset( $bits['i'] ) ) {
+ throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
+ }
+ return $bits;
+ }
+
+ /**
+ * Split a <template> or <tplarg> node
+ */
+ function splitTemplate() {
+ $parts = array();
+ $bits = array( 'lineStart' => '' );
+ for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
+ if ( !isset( $child->name ) ) {
+ continue;
+ }
+ if ( $child->name == 'title' ) {
+ $bits['title'] = $child;
+ }
+ if ( $child->name == 'part' ) {
+ $parts[] = $child;
+ }
+ if ( $child->name == 'lineStart' ) {
+ $bits['lineStart'] = '1';
+ }
+ }
+ if ( !isset( $bits['title'] ) ) {
+ throw new MWException( 'Invalid node passed to ' . __METHOD__ );
+ }
+ $bits['parts'] = new PPNode_Hash_Array( $parts );
+ return $bits;
+ }
+}
+
+/**
+ * @ingroup Parser
+ */
+class PPNode_Hash_Text implements PPNode {
+ var $value, $nextSibling;
+
+ function __construct( $value ) {
+ if ( is_object( $value ) ) {
+ throw new MWException( __CLASS__ . ' given object instead of string' );
+ }
+ $this->value = $value;
+ }
+
+ function __toString() {
+ return htmlspecialchars( $this->value );
+ }
+
+ function getNextSibling() {
+ return $this->nextSibling;
+ }
+
+ function getChildren() { return false; }
+ function getFirstChild() { return false; }
+ function getChildrenOfType( $name ) { return false; }
+ function getLength() { return false; }
+ function item( $i ) { return false; }
+ function getName() { return '#text'; }
+ function splitArg() { throw new MWException( __METHOD__ . ': not supported' ); }
+ function splitExt() { throw new MWException( __METHOD__ . ': not supported' ); }
+ function splitHeading() { throw new MWException( __METHOD__ . ': not supported' ); }
+}
+
+/**
+ * @ingroup Parser
+ */
+class PPNode_Hash_Array implements PPNode {
+ var $value, $nextSibling;
+
+ function __construct( $value ) {
+ $this->value = $value;
+ }
+
+ function __toString() {
+ return var_export( $this, true );
+ }
+
+ function getLength() {
+ return count( $this->value );
+ }
+
+ function item( $i ) {
+ return $this->value[$i];
+ }
+
+ function getName() { return '#nodelist'; }
+
+ function getNextSibling() {
+ return $this->nextSibling;
+ }
+
+ function getChildren() { return false; }
+ function getFirstChild() { return false; }
+ function getChildrenOfType( $name ) { return false; }
+ function splitArg() { throw new MWException( __METHOD__ . ': not supported' ); }
+ function splitExt() { throw new MWException( __METHOD__ . ': not supported' ); }
+ function splitHeading() { throw new MWException( __METHOD__ . ': not supported' ); }
+}
+
+/**
+ * @ingroup Parser
+ */
+class PPNode_Hash_Attr implements PPNode {
+ var $name, $value, $nextSibling;
+
+ function __construct( $name, $value ) {
+ $this->name = $name;
+ $this->value = $value;
+ }
+
+ function __toString() {
+ return "<@{$this->name}>" . htmlspecialchars( $this->value ) . "</@{$this->name}>";
+ }
+
+ function getName() {
+ return $this->name;
+ }
+
+ function getNextSibling() {
+ return $this->nextSibling;
+ }
+
+ function getChildren() { return false; }
+ function getFirstChild() { return false; }
+ function getChildrenOfType( $name ) { return false; }
+ function getLength() { return false; }
+ function item( $i ) { return false; }
+ function splitArg() { throw new MWException( __METHOD__ . ': not supported' ); }
+ function splitExt() { throw new MWException( __METHOD__ . ': not supported' ); }
+ function splitHeading() { throw new MWException( __METHOD__ . ': not supported' ); }
+}
diff --git a/includes/proxy_check.php b/includes/proxy_check.php
index a878a257..61995fea 100644
--- a/includes/proxy_check.php
+++ b/includes/proxy_check.php
@@ -50,5 +50,3 @@ if ( ( isset( $_REQUEST ) && array_key_exists( 'argv', $_REQUEST ) ) || count( $
$output = escapeshellarg( $output );
#`echo $output >> /home/tstarling/open/proxy.log`;
-
-
diff --git a/includes/specials/SpecialAllmessages.php b/includes/specials/SpecialAllmessages.php
new file mode 100644
index 00000000..c2a8de4e
--- /dev/null
+++ b/includes/specials/SpecialAllmessages.php
@@ -0,0 +1,217 @@
+<?php
+/**
+ * Use this special page to get a list of the MediaWiki system messages.
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Constructor.
+ */
+function wfSpecialAllmessages() {
+ global $wgOut, $wgRequest, $wgMessageCache, $wgTitle;
+ global $wgUseDatabaseMessages;
+
+ # The page isn't much use if the MediaWiki namespace is not being used
+ if( !$wgUseDatabaseMessages ) {
+ $wgOut->addWikiMsg( 'allmessagesnotsupportedDB' );
+ return;
+ }
+
+ wfProfileIn( __METHOD__ );
+
+ wfProfileIn( __METHOD__ . '-setup' );
+ $ot = $wgRequest->getText( 'ot' );
+
+ $navText = wfMsg( 'allmessagestext' );
+
+ # Make sure all extension messages are available
+
+ $wgMessageCache->loadAllMessages();
+
+ $sortedArray = array_merge( Language::getMessagesFor( 'en' ), $wgMessageCache->getExtensionMessagesFor( 'en' ) );
+ ksort( $sortedArray );
+ $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]['msg'] = wfMsgNoTrans( $key );
+ }
+
+ wfProfileOut( __METHOD__ . '-setup' );
+
+ wfProfileIn( __METHOD__ . '-output' );
+ $wgOut->addScriptFile( 'allmessages.js' );
+ if ( $ot == 'php' ) {
+ $navText .= wfAllMessagesMakePhp( $messages );
+ $wgOut->addHTML( 'PHP | <a href="' . $wgTitle->escapeLocalUrl( 'ot=html' ) . '">HTML</a> | ' .
+ '<a href="' . $wgTitle->escapeLocalUrl( 'ot=xml' ) . '">XML</a>' .
+ '<pre>' . htmlspecialchars( $navText ) . '</pre>' );
+ } else if ( $ot == 'xml' ) {
+ $wgOut->disable();
+ header( 'Content-type: text/xml' );
+ echo wfAllMessagesMakeXml( $messages );
+ } else {
+ $wgOut->addHTML( '<a href="' . $wgTitle->escapeLocalUrl( 'ot=php' ) . '">PHP</a> | ' .
+ 'HTML | <a href="' . $wgTitle->escapeLocalUrl( 'ot=xml' ) . '">XML</a>' );
+ $wgOut->addWikiText( $navText );
+ $wgOut->addHTML( wfAllMessagesMakeHTMLText( $messages ) );
+ }
+ wfProfileOut( __METHOD__ . '-output' );
+
+ wfProfileOut( __METHOD__ );
+}
+
+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";
+ }
+ $txt .= "</messages>";
+ return $txt;
+}
+
+/**
+ * Create the messages array, formatted in PHP to copy to language files.
+ * @param $messages Messages array.
+ * @return The PHP messages array.
+ * @todo Make suitable for language files.
+ */
+function wfAllMessagesMakePhp( $messages ) {
+ global $wgLang;
+ $txt = "\n\n\$messages = array(\n";
+ foreach( $messages as $key => $m ) {
+ if( $wgLang->getCode() != 'en' && $m['msg'] == $m['enmsg'] ) {
+ continue;
+ } else if ( wfEmptyMsg( $key, $m['msg'] ) ) {
+ $m['msg'] = '';
+ $comment = ' #empty';
+ } else {
+ $comment = '';
+ }
+ $txt .= "'$key' => '" . preg_replace( '/(?<!\\\\)\'/', "\'", $m['msg']) . "',$comment\n";
+ }
+ $txt .= ');';
+ return $txt;
+}
+
+/**
+ * Create a list of messages, formatted in HTML as a list of messages and values and showing differences between the default language file message and the message in MediaWiki: namespace.
+ * @param $messages Messages array.
+ * @return The HTML list of messages.
+ */
+function wfAllMessagesMakeHTMLText( $messages ) {
+ global $wgLang, $wgContLang, $wgUser;
+ wfProfileIn( __METHOD__ );
+
+ $sk = $wgUser->getSkin();
+ $talk = wfMsg( 'talkpagelinktext' );
+
+ $input = Xml::element( 'input', array(
+ 'type' => 'text',
+ 'id' => 'allmessagesinput',
+ 'onkeyup' => 'allmessagesfilter()'
+ ), '' );
+ $checkbox = Xml::element( 'input', array(
+ 'type' => 'button',
+ 'value' => wfMsgHtml( 'allmessagesmodified' ),
+ 'id' => 'allmessagescheckbox',
+ 'onclick' => 'allmessagesmodified()'
+ ), '' );
+
+ $txt = '<span id="allmessagesfilter" style="display: none;">' . wfMsgHtml( 'allmessagesfilter' ) . " {$input}{$checkbox} " . '</span>';
+
+ $txt .= '
+<table border="1" cellspacing="0" width="100%" id="allmessagestable">
+ <tr>
+ <th rowspan="2">' . wfMsgHtml( 'allmessagesname' ) . '</th>
+ <th>' . wfMsgHtml( 'allmessagesdefault' ) . '</th>
+ </tr>
+ <tr>
+ <th>' . wfMsgHtml( 'allmessagescurrent' ) . '</th>
+ </tr>';
+
+ wfProfileIn( __METHOD__ . "-check" );
+
+ # This is a nasty hack to avoid doing independent existence checks
+ # without sending the links and table through the slow wiki parser.
+ $pageExists = array(
+ NS_MEDIAWIKI => array(),
+ 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 );
+ while( $s = $dbr->fetchObject( $res ) ) {
+ $pageExists[$s->page_namespace][$s->page_title] = true;
+ }
+ $dbr->freeResult( $res );
+ wfProfileOut( __METHOD__ . "-check" );
+
+ wfProfileIn( __METHOD__ . "-output" );
+
+ $i = 0;
+
+ foreach( $messages as $key => $m ) {
+ $title = $wgLang->ucfirst( $key );
+ if( $wgLang->getCode() != $wgContLang->getCode() ) {
+ $title .= '/' . $wgLang->getCode();
+ }
+
+ $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>' );
+ } else {
+ $pageLink = $sk->makeBrokenLinkObj( $titleObj, "<span id=\"sp-allmessages-i-$i\">" . htmlspecialchars( $key ) . '</span>' );
+ }
+ if( isset( $pageExists[NS_MEDIAWIKI_TALK][$title] ) ) {
+ $talkLink = $sk->makeKnownLinkObj( $talkPage, htmlspecialchars( $talk ) );
+ } else {
+ $talkLink = $sk->makeBrokenLinkObj( $talkPage, htmlspecialchars( $talk ) );
+ }
+
+ $anchor = 'msg_' . htmlspecialchars( strtolower( $title ) );
+ $anchor = "<a id=\"$anchor\" name=\"$anchor\"></a>";
+
+ 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>";
+ } else {
+ $txt .= "
+ <tr class=\"def\" id=\"sp-allmessages-r1-$i\">
+ <td>
+ $anchor$pageLink<br />$talkLink
+ </td><td>
+$mw
+ </td>
+ </tr>";
+ }
+ $i++;
+ }
+ $txt .= '</table>';
+ wfProfileOut( __METHOD__ . '-output' );
+
+ wfProfileOut( __METHOD__ );
+ return $txt;
+}
diff --git a/includes/specials/SpecialAllpages.php b/includes/specials/SpecialAllpages.php
new file mode 100644
index 00000000..7223e317
--- /dev/null
+++ b/includes/specials/SpecialAllpages.php
@@ -0,0 +1,404 @@
+<?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 {
+ /**
+ * Maximum number of pages to show on single subpage.
+ */
+ protected $maxPerPage = 960;
+
+ /**
+ * Name of this special page. Used to make title objects that reference back
+ * to this page.
+ */
+ protected $name = 'Allpages';
+
+ /**
+ * 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;
+}
+
+/**
+ * @param integer $namespace (default NS_MAIN)
+ */
+function showToplevel ( $namespace = NS_MAIN, $including = false ) {
+ 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 );
+
+ global $wgMemc;
+ $key = wfMemcKey( 'allpages', 'ns', $namespace );
+ $lines = $wgMemc->get( $key );
+
+ if( !is_array( $lines ) ) {
+ $options = array( 'LIMIT' => 1 );
+ if ( ! $dbr->implicitOrderby() ) {
+ $options['ORDER BY'] = 'page_title';
+ }
+ $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;
+ }
+ if( $s = $dbr->fetchObject( $res ) ) {
+ 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;
+ }
+ $dbr->freeResult( $res );
+ }
+ $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, 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 />';
+ } else {
+ $out2 = $nsForm . '<hr />';
+ }
+ }
+
+ $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>';
+}
+
+/**
+ * @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;
+
+ $sk = $wgUser->getSkin();
+
+ $fromList = $this->getNamespaceKeyAndText($namespace, $from);
+ $namespaces = $wgContLang->getNamespaces();
+ $align = $wgContLang->isRtl() ? 'left' : 'right';
+
+ $n = 0;
+
+ if ( !$fromList ) {
+ $out = wfMsgWikiHtml( 'allpagesbadtitle' );
+ } elseif ( !in_array( $namespace, array_keys( $namespaces ) ) ) {
+ // Show errormessage and reset to NS_MAIN
+ $out = wfMsgExt( 'allpages-bad-ns', array( 'parseinline' ), $namespace );
+ $namespace = NS_MAIN;
+ } else {
+ list( $namespace, $fromKey, $from ) = $fromList;
+
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select( 'page',
+ array( 'page_namespace', 'page_title', 'page_is_redirect' ),
+ array(
+ 'page_namespace' => $namespace,
+ 'page_title >= ' . $dbr->addQuotes( $fromKey )
+ ),
+ __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 = $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>';
+ }
+ $out .= "<td width=\"33%\">$link</td>";
+ $n++;
+ if( $n % 3 == 0 ) {
+ $out .= '</tr>';
+ }
+ }
+ if( ($n % 3) != 0 ) {
+ $out .= '</tr>';
+ }
+ $out .= '</table>';
+ } else {
+ $out = '';
+ }
+ }
+
+ if ( $including ) {
+ $out2 = '';
+ } else {
+ if( $from == '' ) {
+ // First chunk; no previous link.
+ $prevTitle = null;
+ } 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 );
+ } 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 );
+ } 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;
+ }
+
+ 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 />";
+ }
+
+ $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;
+ }
+}
+}
diff --git a/includes/specials/SpecialAncientpages.php b/includes/specials/SpecialAncientpages.php
new file mode 100644
index 00000000..188ad914
--- /dev/null
+++ b/includes/specials/SpecialAncientpages.php
@@ -0,0 +1,60 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Implements Special:Ancientpages
+ * @ingroup SpecialPage
+ */
+class AncientPagesPage extends QueryPage {
+
+ function getName() {
+ return "Ancientpages";
+ }
+
+ function isExpensive() {
+ return true;
+ }
+
+ function isSyndicated() { return false; }
+
+ function getSQL() {
+ global $wgDBtype;
+ $db = wfGetDB( DB_SLAVE );
+ $page = $db->tableName( 'page' );
+ $revision = $db->tableName( 'revision' );
+ $epoch = $wgDBtype == 'mysql' ? 'UNIX_TIMESTAMP(rev_timestamp)' :
+ 'EXTRACT(epoch FROM rev_timestamp)';
+ return
+ "SELECT 'Ancientpages' as type,
+ page_namespace as namespace,
+ page_title as title,
+ $epoch as value
+ FROM $page, $revision
+ WHERE page_namespace=".NS_MAIN." AND page_is_redirect=0
+ AND page_latest=rev_id";
+ }
+
+ function sortDescending() {
+ return false;
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgLang, $wgContLang;
+
+ $d = $wgLang->timeanddate( wfTimestamp( TS_MW, $result->value ), true );
+ $title = Title::makeTitle( $result->namespace, $result->title );
+ $link = $skin->makeKnownLinkObj( $title, htmlspecialchars( $wgContLang->convert( $title->getPrefixedText() ) ) );
+ return wfSpecialList($link, $d);
+ }
+}
+
+function wfSpecialAncientpages() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $app = new AncientPagesPage();
+
+ $app->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialBlankpage.php b/includes/specials/SpecialBlankpage.php
new file mode 100644
index 00000000..fdabe49d
--- /dev/null
+++ b/includes/specials/SpecialBlankpage.php
@@ -0,0 +1,6 @@
+<?php
+
+function wfSpecialBlankpage() {
+ global $wgOut;
+ $wgOut->addHTML(wfMsg('intentionallyblankpage'));
+}
diff --git a/includes/specials/SpecialBlockip.php b/includes/specials/SpecialBlockip.php
new file mode 100644
index 00000000..52829d92
--- /dev/null
+++ b/includes/specials/SpecialBlockip.php
@@ -0,0 +1,494 @@
+<?php
+/**
+ * Constructor for Special:Blockip page
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Constructor
+ */
+function wfSpecialBlockip( $par ) {
+ global $wgUser, $wgOut, $wgRequest;
+
+ # Can't block when the database is locked
+ if( wfReadOnly() ) {
+ $wgOut->readOnlyPage();
+ return;
+ }
+
+ # Permission check
+ if( !$wgUser->isAllowed( 'block' ) ) {
+ $wgOut->permissionRequired( 'block' );
+ return;
+ }
+
+ $ipb = new IPBlockForm( $par );
+
+ $action = $wgRequest->getVal( 'action' );
+ if ( 'success' == $action ) {
+ $ipb->showSuccess();
+ } else if ( $wgRequest->wasPosted() && 'submit' == $action &&
+ $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) {
+ $ipb->doSubmit();
+ } else {
+ $ipb->showForm( '' );
+ }
+}
+
+/**
+ * Form object for the Special:Blockip page.
+ *
+ * @ingroup SpecialPage
+ */
+class IPBlockForm {
+ var $BlockAddress, $BlockExpiry, $BlockReason;
+# var $BlockEmail;
+
+ function IPBlockForm( $par ) {
+ global $wgRequest, $wgUser;
+
+ $this->BlockAddress = $wgRequest->getVal( 'wpBlockAddress', $wgRequest->getVal( 'ip', $par ) );
+ $this->BlockAddress = strtr( $this->BlockAddress, '_', ' ' );
+ $this->BlockReason = $wgRequest->getText( 'wpBlockReason' );
+ $this->BlockReasonList = $wgRequest->getText( 'wpBlockReasonList' );
+ $this->BlockExpiry = $wgRequest->getVal( 'wpBlockExpiry', wfMsg('ipbotheroption') );
+ $this->BlockOther = $wgRequest->getVal( 'wpBlockOther', '' );
+
+ # Unchecked checkboxes are not included in the form data at all, so having one
+ # that is true by default is a bit tricky
+ $byDefault = !$wgRequest->wasPosted();
+ $this->BlockAnonOnly = $wgRequest->getBool( 'wpAnonOnly', $byDefault );
+ $this->BlockCreateAccount = $wgRequest->getBool( 'wpCreateAccount', $byDefault );
+ $this->BlockEnableAutoblock = $wgRequest->getBool( 'wpEnableAutoblock', $byDefault );
+ $this->BlockEmail = $wgRequest->getBool( 'wpEmailBan', false );
+ $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;
+ }
+
+ function showForm( $err ) {
+ global $wgOut, $wgUser, $wgSysopUserBans;
+
+ $wgOut->setPagetitle( wfMsg( 'blockip' ) );
+ $wgOut->addWikiMsg( 'blockiptext' );
+
+ if($wgSysopUserBans) {
+ $mIpaddress = Xml::label( wfMsg( 'ipadressorusername' ), 'mw-bi-target' );
+ } else {
+ $mIpaddress = Xml::label( wfMsg( 'ipaddress' ), 'mw-bi-target' );
+ }
+ $mIpbexpiry = Xml::label( wfMsg( 'ipbexpiry' ), 'wpBlockExpiry' );
+ $mIpbother = Xml::label( wfMsg( 'ipbother' ), 'mw-bi-other' );
+ $mIpbreasonother = Xml::label( wfMsg( 'ipbreason' ), 'wpBlockReasonList' );
+ $mIpbreason = Xml::label( wfMsg( 'ipbotherreason' ), 'mw-bi-reason' );
+
+ $titleObj = SpecialPage::getTitleFor( 'Blockip' );
+
+ if ( "" != $err ) {
+ $wgOut->setSubtitle( wfMsgHtml( 'formerror' ) );
+ $wgOut->addHTML( Xml::tags( 'p', array( 'class' => 'error' ), $err ) );
+ }
+
+ $scBlockExpiryOptions = wfMsgForContent( 'ipboptions' );
+
+ $showblockoptions = $scBlockExpiryOptions != '-';
+ if (!$showblockoptions)
+ $mIpbother = $mIpbexpiry;
+
+ $blockExpiryFormOptions = Xml::option( wfMsg( 'ipbotheroption' ), 'other' );
+ foreach (explode(',', $scBlockExpiryOptions) as $option) {
+ if ( strpos($option, ":") === false ) $option = "$option:$option";
+ list($show, $value) = explode(":", $option);
+ $show = htmlspecialchars($show);
+ $value = htmlspecialchars($value);
+ $blockExpiryFormOptions .= Xml::option( $show, $value, $this->BlockExpiry === $value ? true : false ) . "\n";
+ }
+
+ $reasonDropDown = Xml::listDropDown( 'wpBlockReasonList',
+ wfMsgForContent( 'ipbreason-dropdown' ),
+ wfMsgForContent( 'ipbreasonotherlist' ), '', 'wpBlockDropDown', 4 );
+
+ global $wgStylePath, $wgStyleVersion;
+ $wgOut->addHTML(
+ Xml::tags( 'script', array( 'type' => 'text/javascript', 'src' => "$wgStylePath/common/block.js?$wgStyleVersion" ), '' ) .
+ Xml::openElement( 'form', array( 'method' => 'post', 'action' => $titleObj->getLocalURL( "action=submit" ), 'id' => 'blockip' ) ) .
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', null, wfMsg( 'blockip-legend' ) ) .
+ Xml::openElement( 'table', array ( 'border' => '0', 'id' => 'mw-blockip-table' ) ) .
+ "<tr>
+ <td class='mw-label'>
+ {$mIpaddress}
+ </td>
+ <td class='mw-input'>" .
+ Xml::input( 'wpBlockAddress', 45, $this->BlockAddress,
+ array(
+ 'tabindex' => '1',
+ 'id' => 'mw-bi-target',
+ 'onchange' => 'updateBlockOptions()' ) ). "
+ </td>
+ </tr>
+ <tr>"
+ );
+ if ( $showblockoptions ) {
+ $wgOut->addHTML("
+ <td class='mw-label'>
+ {$mIpbexpiry}
+ </td>
+ <td class='mw-input'>" .
+ Xml::tags( 'select',
+ array(
+ 'id' => 'wpBlockExpiry',
+ 'name' => 'wpBlockExpiry',
+ 'onchange' => 'considerChangingExpiryFocus()',
+ 'tabindex' => '2' ),
+ $blockExpiryFormOptions ) .
+ "</td>"
+ );
+ }
+ $wgOut->addHTML("
+ </tr>
+ <tr id='wpBlockOther'>
+ <td class='mw-label'>
+ {$mIpbother}
+ </td>
+ <td class='mw-input'>" .
+ Xml::input( 'wpBlockOther', 45, $this->BlockOther,
+ array( 'tabindex' => '3', 'id' => 'mw-bi-other' ) ) . "
+ </td>
+ </tr>
+ <tr>
+ <td class='mw-label'>
+ {$mIpbreasonother}
+ </td>
+ <td class='mw-input'>
+ {$reasonDropDown}
+ </td>
+ </tr>
+ <tr id=\"wpBlockReason\">
+ <td class='mw-label'>
+ {$mIpbreason}
+ </td>
+ <td class='mw-input'>" .
+ Xml::input( 'wpBlockReason', 45, $this->BlockReason,
+ array( 'tabindex' => '5', 'id' => 'mw-bi-reason', 'maxlength'=> '200' ) ) . "
+ </td>
+ </tr>
+ <tr id='wpAnonOnlyRow'>
+ <td>&nbsp;</td>
+ <td class='mw-input'>" .
+ Xml::checkLabel( wfMsg( 'ipbanononly' ),
+ 'wpAnonOnly', 'wpAnonOnly', $this->BlockAnonOnly,
+ array( 'tabindex' => '6' ) ) . "
+ </td>
+ </tr>
+ <tr id='wpCreateAccountRow'>
+ <td>&nbsp;</td>
+ <td class='mw-input'>" .
+ Xml::checkLabel( wfMsg( 'ipbcreateaccount' ),
+ 'wpCreateAccount', 'wpCreateAccount', $this->BlockCreateAccount,
+ array( 'tabindex' => '7' ) ) . "
+ </td>
+ </tr>
+ <tr id='wpEnableAutoblockRow'>
+ <td>&nbsp;</td>
+ <td class='mw-input'>" .
+ Xml::checkLabel( wfMsg( 'ipbenableautoblock' ),
+ 'wpEnableAutoblock', 'wpEnableAutoblock', $this->BlockEnableAutoblock,
+ array( 'tabindex' => '8' ) ) . "
+ </td>
+ </tr>"
+ );
+
+ global $wgSysopEmailBans;
+ if ( $wgSysopEmailBans && $wgUser->isAllowed( 'blockemail' ) ) {
+ $wgOut->addHTML("
+ <tr id='wpEnableEmailBan'>
+ <td>&nbsp;</td>
+ <td class='mw-input'>" .
+ Xml::checkLabel( wfMsg( 'ipbemailban' ),
+ 'wpEmailBan', 'wpEmailBan', $this->BlockEmail,
+ array( 'tabindex' => '9' )) . "
+ </td>
+ </tr>"
+ );
+ }
+
+ // Allow some users to hide name from block log, blocklist and listusers
+ if ( $wgUser->isAllowed( 'hideuser' ) ) {
+ $wgOut->addHTML("
+ <tr id='wpEnableHideUser'>
+ <td>&nbsp;</td>
+ <td class='mw-input'>" .
+ Xml::checkLabel( wfMsg( 'ipbhidename' ),
+ 'wpHideName', 'wpHideName', $this->BlockHideName,
+ array( 'tabindex' => '10' ) ) . "
+ </td>
+ </tr>"
+ );
+ }
+
+ # Watchlist their user page?
+ $wgOut->addHTML("
+ <tr id='wpEnableWatchUser'>
+ <td>&nbsp;</td>
+ <td class='mw-input'>" .
+ Xml::checkLabel( wfMsg( 'ipbwatchuser' ),
+ 'wpWatchUser', 'wpWatchUser', $this->BlockWatchUser,
+ array( 'tabindex' => '11' ) ) . "
+ </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' ) ) . "
+ </td>
+ </tr>" .
+ Xml::closeElement( 'table' ) .
+ Xml::hidden( 'wpEditToken', $wgUser->editToken() ) .
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' ) .
+ Xml::tags( 'script', array( 'type' => 'text/javascript' ), 'updateBlockOptions()' ) . "\n"
+ );
+
+ $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 ) ) {
+ $this->showLogFragment( $wgOut, Title::makeTitle( NS_USER, $this->BlockAddress ) );
+ } elseif( preg_match( '/^\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}/', $this->BlockAddress ) ) {
+ $this->showLogFragment( $wgOut, Title::makeTitle( NS_USER, $this->BlockAddress ) );
+ }
+ }
+
+ /**
+ * Backend block code.
+ * $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;
+
+ $userId = 0;
+ # Expand valid IPv6 addresses, usernames are left as is
+ $this->BlockAddress = IP::sanitizeIP( $this->BlockAddress );
+ # isIPv4() and IPv6() are used for final validation
+ $rxIP4 = '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}';
+ $rxIP6 = '\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}';
+ $rxIP = "($rxIP4|$rxIP6)";
+
+ # Check for invalid specifications
+ if ( !preg_match( "/^$rxIP$/", $this->BlockAddress ) ) {
+ $matches = array();
+ if ( preg_match( "/^($rxIP4)\\/(\\d{1,2})$/", $this->BlockAddress, $matches ) ) {
+ # IPv4
+ if ( $wgSysopRangeBans ) {
+ if ( !IP::isIPv4( $this->BlockAddress ) || $matches[2] < 16 || $matches[2] > 32 ) {
+ return array('ip_range_invalid');
+ }
+ $this->BlockAddress = Block::normaliseRange( $this->BlockAddress );
+ } else {
+ # Range block illegal
+ return array('range_block_disabled');
+ }
+ } else if ( preg_match( "/^($rxIP6)\\/(\\d{1,3})$/", $this->BlockAddress, $matches ) ) {
+ # IPv6
+ if ( $wgSysopRangeBans ) {
+ if ( !IP::isIPv6( $this->BlockAddress ) || $matches[2] < 64 || $matches[2] > 128 ) {
+ return array('ip_range_invalid');
+ }
+ $this->BlockAddress = Block::normaliseRange( $this->BlockAddress );
+ } else {
+ # Range block illegal
+ return array('range_block_disabled');
+ }
+ } else {
+ # Username block
+ if ( $wgSysopUserBans ) {
+ $user = User::newFromName( $this->BlockAddress );
+ if( !is_null( $user ) && $user->getId() ) {
+ # Use canonical name
+ $userId = $user->getId();
+ $this->BlockAddress = $user->getName();
+ } else {
+ return array('nosuchusershort', htmlspecialchars( $user ? $user->getName() : $this->BlockAddress ) );
+ }
+ } else {
+ return array('badipaddress');
+ }
+ }
+ }
+
+ $reasonstr = $this->BlockReasonList;
+ if ( $reasonstr != 'other' && $this->BlockReason != '') {
+ // Entry from drop down menu + additional comment
+ $reasonstr .= ': ' . $this->BlockReason;
+ } elseif ( $reasonstr == 'other' ) {
+ $reasonstr = $this->BlockReason;
+ }
+
+ $expirestr = $this->BlockExpiry;
+ if( $expirestr == 'other' )
+ $expirestr = $this->BlockOther;
+
+ if ((strlen($expirestr) == 0) || (strlen($expirestr) > 50)) {
+ return array('ipb_expiry_invalid');
+ }
+
+ if ( false === ($expiry = Block::parseExpiryInput( $expirestr )) ) {
+ // Bad expiry.
+ return array('ipb_expiry_invalid');
+ }
+
+ if( $this->BlockHideName && $expiry != 'infinity' ) {
+ // Bad expiry.
+ return array('ipb_expiry_temp');
+ }
+
+ # Create block
+ # Note: for a user block, ipb_address is only for display purposes
+ $block = new Block( $this->BlockAddress, $userId, $wgUser->getId(),
+ $reasonstr, wfTimestampNow(), 0, $expiry, $this->BlockAnonOnly,
+ $this->BlockCreateAccount, $this->BlockEnableAutoblock, $this->BlockHideName,
+ $this->BlockEmail );
+
+ if ( wfRunHooks('BlockIp', array(&$block, &$wgUser)) ) {
+
+ if ( !$block->insert() ) {
+ return array('ipb_already_blocked', htmlspecialchars($this->BlockAddress));
+ }
+
+ wfRunHooks('BlockIpComplete', array($block, $wgUser));
+
+ if ( $this->BlockWatchUser ) {
+ $wgUser->addWatch ( Title::makeTitle( NS_USER, $this->BlockAddress ) );
+ }
+
+ # Prepare log parameters
+ $logParams = array();
+ $logParams[] = $expirestr;
+ $logParams[] = $this->blockLogFlags();
+
+ # 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 ),
+ $reasonstr, $logParams );
+
+ # Report to the user
+ return array();
+ }
+ else
+ return array('hookaborted');
+ }
+
+ /**
+ * UI entry point for blocking
+ * Wraps around doBlock()
+ */
+ function doSubmit()
+ {
+ global $wgOut;
+ $retval = $this->doBlock();
+ if(empty($retval)) {
+ $titleObj = SpecialPage::getTitleFor( 'Blockip' );
+ $wgOut->redirect( $titleObj->getFullURL( 'action=success&ip=' .
+ urlencode( $this->BlockAddress ) ) );
+ return;
+ }
+ $key = array_shift($retval);
+ $this->showForm(wfMsgReal($key, $retval));
+ }
+
+ function showSuccess() {
+ global $wgOut;
+
+ $wgOut->setPagetitle( wfMsg( 'blockip' ) );
+ $wgOut->setSubtitle( wfMsg( 'blockipsuccesssub' ) );
+ $text = wfMsgExt( 'blockipsuccesstext', array( 'parse' ), $this->BlockAddress );
+ $wgOut->addHtml( $text );
+ }
+
+ function showLogFragment( $out, $title ) {
+ $out->addHtml( Xml::element( 'h2', NULL, LogPage::logName( 'block' ) ) );
+ LogEventsList::showLogExtract( $out, 'block', $title->getPrefixedText() );
+ }
+
+ /**
+ * Return a comma-delimited list of "flags" to be passed to the log
+ * reader for this block, to provide more information in the logs
+ *
+ * @return array
+ */
+ private function blockLogFlags() {
+ $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
+ $flags[] = 'anononly';
+ if( $this->BlockCreateAccount )
+ $flags[] = 'nocreate';
+ if( !$this->BlockEnableAutoblock )
+ $flags[] = 'noautoblock';
+ if ( $this->BlockEmail )
+ $flags[] = 'noemail';
+ return implode( ',', $flags );
+ }
+
+ /**
+ * Builds unblock and block list links
+ *
+ * @return string
+ */
+ private function getConvenienceLinks() {
+ global $wgUser;
+ $skin = $wgUser->getSkin();
+ $links[] = $skin->makeLink ( 'MediaWiki:Ipbreason-dropdown', wfMsgHtml( 'ipb-edit-dropdown' ) );
+ $links[] = $this->getUnblockLink( $skin );
+ $links[] = $this->getBlockListLink( $skin );
+ return '<p class="mw-ipb-conveniencelinks">' . implode( ' | ', $links ) . '</p>';
+ }
+
+ /**
+ * Build a convenient link to unblock the given username or IP
+ * address, if available; otherwise link to a blank unblock
+ * form
+ *
+ * @param $skin Skin to use
+ * @return string
+ */
+ private function getUnblockLink( $skin ) {
+ $list = SpecialPage::getTitleFor( 'Ipblocklist' );
+ if( $this->BlockAddress ) {
+ $addr = htmlspecialchars( strtr( $this->BlockAddress, '_', ' ' ) );
+ return $skin->makeKnownLinkObj( $list, wfMsgHtml( 'ipb-unblock-addr', $addr ),
+ 'action=unblock&ip=' . urlencode( $this->BlockAddress ) );
+ } else {
+ return $skin->makeKnownLinkObj( $list, wfMsgHtml( 'ipb-unblock' ), 'action=unblock' );
+ }
+ }
+
+ /**
+ * Build a convenience link to the block list
+ *
+ * @param $skin Skin to use
+ * @return string
+ */
+ private function getBlockListLink( $skin ) {
+ $list = SpecialPage::getTitleFor( 'Ipblocklist' );
+ if( $this->BlockAddress ) {
+ $addr = htmlspecialchars( strtr( $this->BlockAddress, '_', ' ' ) );
+ return $skin->makeKnownLinkObj( $list, wfMsgHtml( 'ipb-blocklist-addr', $addr ),
+ 'ip=' . urlencode( $this->BlockAddress ) );
+ } else {
+ return $skin->makeKnownLinkObj( $list, wfMsgHtml( 'ipb-blocklist' ) );
+ }
+ }
+}
diff --git a/includes/specials/SpecialBlockme.php b/includes/specials/SpecialBlockme.php
new file mode 100644
index 00000000..f222e3c6
--- /dev/null
+++ b/includes/specials/SpecialBlockme.php
@@ -0,0 +1,37 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ *
+ */
+function wfSpecialBlockme() {
+ global $wgRequest, $wgBlockOpenProxies, $wgOut, $wgProxyKey;
+
+ $ip = wfGetIP();
+
+ if( !$wgBlockOpenProxies || $wgRequest->getText( 'ip' ) != md5( $ip . $wgProxyKey ) ) {
+ $wgOut->addWikiMsg( 'proxyblocker-disabled' );
+ return;
+ }
+
+ $blockerName = wfMsg( "proxyblocker" );
+ $reason = wfMsg( "proxyblockreason" );
+
+ $u = User::newFromName( $blockerName );
+ $id = $u->idForName();
+ if ( !$id ) {
+ $u = User::newFromName( $blockerName );
+ $u->addToDatabase();
+ $u->setPassword( bin2hex( mt_rand(0, 0x7fffffff ) ) );
+ $u->saveSettings();
+ $id = $u->getID();
+ }
+
+ $block = new Block( $ip, 0, $id, $reason, wfTimestampNow() );
+ $block->insert();
+
+ $wgOut->addWikiMsg( "proxyblocksuccess" );
+}
diff --git a/includes/specials/SpecialBooksources.php b/includes/specials/SpecialBooksources.php
new file mode 100644
index 00000000..0690c5c0
--- /dev/null
+++ b/includes/specials/SpecialBooksources.php
@@ -0,0 +1,110 @@
+<?php
+
+/**
+ * Special page outputs information on sourcing a book with a particular ISBN
+ * The parser creates links to this page when dealing with ISBNs in wikitext
+ *
+ * @author Rob Church <robchur@gmail.com>
+ * @todo Validate ISBNs using the standard check-digit method
+ * @ingroup SpecialPages
+ */
+class SpecialBookSources extends SpecialPage {
+
+ /**
+ * ISBN passed to the page, if any
+ */
+ private $isbn = '';
+
+ /**
+ * Constructor
+ */
+ public function __construct() {
+ parent::__construct( 'Booksources' );
+ }
+
+ /**
+ * Show the special page
+ *
+ * @param $isbn ISBN passed as a subpage parameter
+ */
+ public function execute( $isbn ) {
+ global $wgOut, $wgRequest;
+ $this->setHeaders();
+ $this->isbn = $this->cleanIsbn( $isbn ? $isbn : $wgRequest->getText( 'isbn' ) );
+ $wgOut->addWikiMsg( 'booksources-summary' );
+ $wgOut->addHtml( $this->makeForm() );
+ if( strlen( $this->isbn ) > 0 )
+ $this->showList();
+ }
+
+ /**
+ * Trim ISBN and remove characters which aren't required
+ *
+ * @param $isbn Unclean ISBN
+ * @return string
+ */
+ private function cleanIsbn( $isbn ) {
+ return trim( preg_replace( '![^0-9X]!', '', $isbn ) );
+ }
+
+ /**
+ * Generate a form to allow users to enter an ISBN
+ *
+ * @return string
+ */
+ private function makeForm() {
+ global $wgScript;
+ $title = self::getTitleFor( 'Booksources' );
+ $form = '<fieldset><legend>' . wfMsgHtml( 'booksources-search-legend' ) . '</legend>';
+ $form .= Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) );
+ $form .= Xml::hidden( 'title', $title->getPrefixedText() );
+ $form .= '<p>' . Xml::inputLabel( wfMsg( 'booksources-isbn' ), 'isbn', 'isbn', 20, $this->isbn );
+ $form .= '&nbsp;' . Xml::submitButton( wfMsg( 'booksources-go' ) ) . '</p>';
+ $form .= Xml::closeElement( 'form' );
+ $form .= '</fieldset>';
+ return $form;
+ }
+
+ /**
+ * Determine where to get the list of book sources from,
+ * format and output them
+ *
+ * @return string
+ */
+ private function showList() {
+ global $wgOut, $wgContLang;
+
+ # Hook to allow extensions to insert additional HTML,
+ # e.g. for API-interacting plugins and so on
+ wfRunHooks( 'BookInformation', array( $this->isbn, &$wgOut ) );
+
+ # Check for a local page such as Project:Book_sources and use that if available
+ $title = Title::makeTitleSafe( NS_PROJECT, wfMsgForContent( 'booksources' ) ); # Show list in content language
+ if( is_object( $title ) && $title->exists() ) {
+ $rev = Revision::newFromTitle( $title );
+ $wgOut->addWikiText( str_replace( 'MAGICNUMBER', $this->isbn, $rev->getText() ) );
+ return true;
+ }
+
+ # Fall back to the defaults given in the language file
+ $wgOut->addWikiMsg( 'booksources-text' );
+ $wgOut->addHtml( '<ul>' );
+ $items = $wgContLang->getBookstoreList();
+ foreach( $items as $label => $url )
+ $wgOut->addHtml( $this->makeListItem( $label, $url ) );
+ $wgOut->addHtml( '</ul>' );
+ return true;
+ }
+
+ /**
+ * Format a book source list item
+ *
+ * @param $label Book source label
+ * @param $url Book source URL
+ * @return string
+ */
+ private function makeListItem( $label, $url ) {
+ $url = str_replace( '$1', $this->isbn, $url );
+ return '<li><a href="' . htmlspecialchars( $url ) . '">' . htmlspecialchars( $label ) . '</a></li>';
+ }
+}
diff --git a/includes/specials/SpecialBrokenRedirects.php b/includes/specials/SpecialBrokenRedirects.php
new file mode 100644
index 00000000..0a16e6de
--- /dev/null
+++ b/includes/specials/SpecialBrokenRedirects.php
@@ -0,0 +1,93 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * A special page listing redirects to non existent page. Those should be
+ * fixed to point to an existing page.
+ * @ingroup SpecialPage
+ */
+class BrokenRedirectsPage extends PageQueryPage {
+ var $targets = array();
+
+ function getName() {
+ return 'BrokenRedirects';
+ }
+
+ function isExpensive( ) { return true; }
+ function isSyndicated() { return false; }
+
+ function getPageHeader( ) {
+ return wfMsgExt( 'brokenredirectstext', array( 'parse' ) );
+ }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ list( $page, $redirect ) = $dbr->tableNamesN( 'page', 'redirect' );
+
+ $sql = "SELECT 'BrokenRedirects' AS type,
+ p1.page_namespace AS namespace,
+ p1.page_title AS title,
+ rd_namespace,
+ rd_title
+ FROM $redirect AS rd
+ JOIN $page p1 ON (rd.rd_from=p1.page_id)
+ LEFT JOIN $page AS p2 ON (rd_namespace=p2.page_namespace AND rd_title=p2.page_title )
+ WHERE rd_namespace >= 0
+ AND p2.page_namespace IS NULL";
+ return $sql;
+ }
+
+ function getOrder() {
+ return '';
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgUser, $wgContLang;
+
+ $fromObj = Title::makeTitle( $result->namespace, $result->title );
+ if ( isset( $result->rd_title ) ) {
+ $toObj = Title::makeTitle( $result->rd_namespace, $result->rd_title );
+ } else {
+ $blinks = $fromObj->getBrokenLinksFrom(); # TODO: check for redirect, not for links
+ if ( $blinks ) {
+ $toObj = $blinks[0];
+ } else {
+ $toObj = false;
+ }
+ }
+
+ // $toObj may very easily be false if the $result list is cached
+ if ( !is_object( $toObj ) ) {
+ return '<s>' . $skin->makeLinkObj( $fromObj ) . '</s>';
+ }
+
+ $from = $skin->makeKnownLinkObj( $fromObj ,'', 'redirect=no' );
+ $edit = $skin->makeKnownLinkObj( $fromObj, wfMsgHtml( 'brokenredirects-edit' ), 'action=edit' );
+ $to = $skin->makeBrokenLinkObj( $toObj );
+ $arr = $wgContLang->getArrow();
+
+ $out = "{$from} {$edit}";
+
+ if( $wgUser->isAllowed( 'delete' ) ) {
+ $delete = $skin->makeKnownLinkObj( $fromObj, wfMsgHtml( 'brokenredirects-delete' ), 'action=delete' );
+ $out .= " {$delete}";
+ }
+
+ $out .= " {$arr} {$to}";
+ return $out;
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialBrokenRedirects() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $sbr = new BrokenRedirectsPage();
+
+ return $sbr->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialCategories.php b/includes/specials/SpecialCategories.php
new file mode 100644
index 00000000..951c2228
--- /dev/null
+++ b/includes/specials/SpecialCategories.php
@@ -0,0 +1,112 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+function wfSpecialCategories( $par=null ) {
+ global $wgOut, $wgRequest;
+
+ if( $par == '' ) {
+ $from = $wgRequest->getText( 'from' );
+ } else {
+ $from = $par;
+ }
+ $cap = new CategoryPager( $from );
+ $wgOut->addHTML(
+ wfMsgExt( 'categoriespagetext', array( 'parse' ) ) .
+ $cap->getStartForm( $from ) .
+ $cap->getNavigationBar() .
+ '<ul>' . $cap->getBody() . '</ul>' .
+ $cap->getNavigationBar()
+ );
+}
+
+/**
+ * TODO: Allow sorting by count. We need to have a unique index to do this
+ * properly.
+ *
+ * @ingroup SpecialPage Pager
+ */
+class CategoryPager extends AlphabeticPager {
+ function __construct( $from ) {
+ parent::__construct();
+ $from = str_replace( ' ', '_', $from );
+ if( $from !== '' ) {
+ global $wgCapitalLinks, $wgContLang;
+ if( $wgCapitalLinks ) {
+ $from = $wgContLang->ucfirst( $from );
+ }
+ $this->mOffset = $from;
+ }
+ }
+
+ function getQueryInfo() {
+ global $wgRequest;
+ return array(
+ 'tables' => array( 'category' ),
+ 'fields' => array( 'cat_title','cat_pages' ),
+ 'conds' => array( 'cat_pages > 0' ),
+ 'options' => array( 'USE INDEX' => 'cat_title' ),
+ );
+ }
+
+ function getIndexField() {
+# return array( 'abc' => 'cat_title', 'count' => 'cat_pages' );
+ return 'cat_title';
+ }
+
+ function getDefaultQuery() {
+ parent::getDefaultQuery();
+ unset( $this->mDefaultQuery['from'] );
+ }
+# protected function getOrderTypeMessages() {
+# return array( 'abc' => 'special-categories-sort-abc',
+# 'count' => 'special-categories-sort-count' );
+# }
+
+ protected function getDefaultDirections() {
+# return array( 'abc' => false, 'count' => true );
+ return false;
+ }
+
+ /* Override getBody to apply LinksBatch on resultset before actually outputting anything. */
+ public function getBody() {
+ if (!$this->mQueryDone) {
+ $this->doQuery();
+ }
+ $batch = new LinkBatch;
+
+ $this->mResult->rewind();
+
+ while ( $row = $this->mResult->fetchObject() ) {
+ $batch->addObj( Title::makeTitleSafe( NS_CATEGORY, $row->cat_title ) );
+ }
+ $batch->execute();
+ $this->mResult->rewind();
+ return parent::getBody();
+ }
+
+ function formatRow($result) {
+ global $wgLang;
+ $title = Title::makeTitle( NS_CATEGORY, $result->cat_title );
+ $titleText = $this->getSkin()->makeLinkObj( $title, htmlspecialchars( $title->getText() ) );
+ $count = wfMsgExt( 'nmembers', array( 'parsemag', 'escape' ),
+ $wgLang->formatNum( $result->cat_pages ) );
+ return Xml::tags('li', null, "$titleText ($count)" ) . "\n";
+ }
+
+ public function getStartForm( $from ) {
+ global $wgScript;
+ $t = SpecialPage::getTitleFor( 'Categories' );
+
+ return
+ Xml::tags( 'form', array( 'method' => 'get', 'action' => $wgScript ),
+ Xml::hidden( 'title', $t->getPrefixedText() ) .
+ Xml::fieldset( wfMsg( 'categories' ),
+ Xml::inputLabel( wfMsg( 'categoriesfrom' ),
+ 'from', 'from', 20, $from ) .
+ ' ' .
+ Xml::submitButton( wfMsg( 'allpagessubmit' ) ) ) );
+ }
+}
diff --git a/includes/specials/SpecialConfirmemail.php b/includes/specials/SpecialConfirmemail.php
new file mode 100644
index 00000000..9075fb95
--- /dev/null
+++ b/includes/specials/SpecialConfirmemail.php
@@ -0,0 +1,139 @@
+<?php
+
+/**
+ * Special page allows users to request email confirmation message, and handles
+ * processing of the confirmation code when the link in the email is followed
+ *
+ * @ingroup SpecialPage
+ * @author Brion Vibber
+ * @author Rob Church <robchur@gmail.com>
+ */
+class EmailConfirmation extends UnlistedSpecialPage {
+
+ /**
+ * Constructor
+ */
+ public function __construct() {
+ parent::__construct( 'Confirmemail' );
+ }
+
+ /**
+ * Main execution point
+ *
+ * @param $code Confirmation code passed to the page
+ */
+ function execute( $code ) {
+ global $wgUser, $wgOut;
+ $this->setHeaders();
+ if( empty( $code ) ) {
+ if( $wgUser->isLoggedIn() ) {
+ if( User::isValidEmailAddr( $wgUser->getEmail() ) ) {
+ $this->showRequestForm();
+ } else {
+ $wgOut->addWikiMsg( 'confirmemail_noemail' );
+ }
+ } else {
+ $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 ) );
+ }
+ } else {
+ $this->attemptConfirm( $code );
+ }
+ }
+
+ /**
+ * Show a nice form for the user to request a confirmation mail
+ */
+ function showRequestForm() {
+ global $wgOut, $wgUser, $wgLang, $wgRequest;
+ if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $wgRequest->getText( 'token' ) ) ) {
+ $ok = $wgUser->sendConfirmationMail();
+ if ( WikiError::isError( $ok ) ) {
+ $wgOut->addWikiMsg( 'confirmemail_sendfailed', $ok->toString() );
+ } else {
+ $wgOut->addWikiMsg( 'confirmemail_sent' );
+ }
+ } else {
+ if( $wgUser->isEmailConfirmed() ) {
+ $time = $wgLang->timeAndDate( $wgUser->mEmailAuthenticated, true );
+ $wgOut->addWikiMsg( 'emailauthenticated', $time );
+ }
+ 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 );
+ }
+ }
+
+ /**
+ * Attempt to confirm the user's email address and show success or failure
+ * as needed; if successful, take the user to log in
+ *
+ * @param $code Confirmation code
+ */
+ function attemptConfirm( $code ) {
+ global $wgUser, $wgOut;
+ $user = User::newFromConfirmationCode( $code );
+ if( is_object( $user ) ) {
+ $user->confirmEmail();
+ $user->saveSettings();
+ $message = $wgUser->isLoggedIn() ? 'confirmemail_loggedin' : 'confirmemail_success';
+ $wgOut->addWikiMsg( $message );
+ if( !$wgUser->isLoggedIn() ) {
+ $title = SpecialPage::getTitleFor( 'Userlogin' );
+ $wgOut->returnToMain( true, $title );
+ }
+ } else {
+ $wgOut->addWikiMsg( 'confirmemail_invalid' );
+ }
+ }
+
+}
+
+/**
+ * Special page allows users to cancel an email confirmation using the e-mail
+ * confirmation code
+ *
+ * @ingroup SpecialPage
+ */
+class EmailInvalidation extends UnlistedSpecialPage {
+
+ public function __construct() {
+ parent::__construct( 'Invalidateemail' );
+ }
+
+ function execute( $code ) {
+ $this->setHeaders();
+ $this->attemptInvalidate( $code );
+ }
+
+ /**
+ * Attempt to invalidate the user's email address and show success or failure
+ * as needed; if successful, link to main page
+ *
+ * @param $code Confirmation code
+ */
+ function attemptInvalidate( $code ) {
+ global $wgUser, $wgOut;
+ $user = User::newFromConfirmationCode( $code );
+ if( is_object( $user ) ) {
+ $user->invalidateEmail();
+ $user->saveSettings();
+ $wgOut->addWikiMsg( 'confirmemail_invalidated' );
+ if( !$wgUser->isLoggedIn() ) {
+ $wgOut->returnToMain();
+ }
+ } else {
+ $wgOut->addWikiMsg( 'confirmemail_invalid' );
+ }
+ }
+}
diff --git a/includes/specials/SpecialContributions.php b/includes/specials/SpecialContributions.php
new file mode 100644
index 00000000..4a131f15
--- /dev/null
+++ b/includes/specials/SpecialContributions.php
@@ -0,0 +1,470 @@
+<?php
+/**
+ * Special:Contributions, show user contributions in a paged list
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Pager for Special:Contributions
+ * @ingroup SpecialPage Pager
+ */
+class ContribsPager extends ReverseChronologicalPager {
+ public $mDefaultDirection = true;
+ var $messages, $target;
+ var $namespace = '', $year = '', $month = '', $mDb;
+
+ function __construct( $target, $namespace = false, $year = false, $month = false ) {
+ parent::__construct();
+ foreach( explode( ' ', 'uctop diff newarticle rollbacklink diff hist newpageletter minoreditletter' ) as $msg ) {
+ $this->messages[$msg] = wfMsgExt( $msg, array( 'escape') );
+ }
+ $this->target = $target;
+ $this->namespace = $namespace;
+
+ $year = intval($year);
+ $month = intval($month);
+
+ $this->year = $year > 0 ? $year : false;
+ $this->month = ($month > 0 && $month < 13) ? $month : false;
+ $this->getDateCond();
+
+ $this->mDb = wfGetDB( DB_SLAVE, 'contributions' );
+ }
+
+ 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();
+ $conds = array_merge( array('page_id=rev_page'), $userCond, $this->getNamespaceCond() );
+ $queryInfo = array(
+ 'tables' => array( 'page', 'revision' ),
+ '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) )
+ );
+ wfRunHooks( 'ContribsPager::getQueryInfo', array( &$this, &$queryInfo ) );
+ return $queryInfo;
+ }
+
+ function getUserCond() {
+ $condition = array();
+
+ if ( $this->target == 'newbies' ) {
+ $max = $this->mDb->selectField( 'user', 'max(user_id)', false, __METHOD__ );
+ $condition[] = 'rev_user >' . (int)($max - $max / 100);
+ $index = 'user_timestamp';
+ } else {
+ $condition['rev_user_text'] = $this->target;
+ $index = 'usertext_timestamp';
+ }
+ return array( $index, $condition );
+ }
+
+ function getNamespaceCond() {
+ 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';
+ }
+
+ function getStartBody() {
+ return "<ul>\n";
+ }
+
+ function getEndBody() {
+ return "</ul>\n";
+ }
+
+ /**
+ * 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 roll-
+ * back 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, $wgContLang;
+
+ $sk = $this->getSkin();
+ $rev = new Revision( $row );
+
+ $page = Title::makeTitle( $row->page_namespace, $row->page_title );
+ $link = $sk->makeKnownLinkObj( $page );
+ $difftext = $topmarktext = '';
+ if( $row->rev_id == $row->page_latest ) {
+ $topmarktext .= '<strong>' . $this->messages['uctop'] . '</strong>';
+ if( !$row->page_is_new ) {
+ $difftext .= '(' . $sk->makeKnownLinkObj( $page, $this->messages['diff'], 'diff=0' ) . ')';
+ } else {
+ $difftext .= $this->messages['newarticle'];
+ }
+
+ if( !$page->getUserPermissionsErrors( 'rollback', $wgUser )
+ && !$page->getUserPermissionsErrors( 'edit', $wgUser ) ) {
+ $topmarktext .= ' '.$sk->generateRollback( $rev );
+ }
+
+ }
+ # Is there a visible previous revision?
+ if( $rev->userCan(Revision::DELETED_TEXT) ) {
+ $difftext = '(' . $sk->makeKnownLinkObj( $page, $this->messages['diff'], 'diff=prev&oldid='.$row->rev_id ) . ')';
+ } else {
+ $difftext = '(' . $this->messages['diff'] . ')';
+ }
+ $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 );
+
+ if( $this->target == 'newbies' ) {
+ $userlink = ' . . ' . $sk->userLink( $row->rev_user, $row->rev_user_text );
+ $userlink .= ' (' . $sk->userTalkLink( $row->rev_user, $row->rev_user_text ) . ') ';
+ } else {
+ $userlink = '';
+ }
+
+ if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
+ $d = '<span class="history-deleted">' . $d . '</span>';
+ }
+
+ if( $rev->getParentId() === 0 ) {
+ $nflag = '<span class="newpage">' . $this->messages['newpageletter'] . '</span>';
+ } else {
+ $nflag = '';
+ }
+
+ if( $row->rev_minor_edit ) {
+ $mflag = '<span class="minor">' . $this->messages['minoreditletter'] . '</span> ';
+ } else {
+ $mflag = '';
+ }
+
+ $ret = "{$d} {$histlink} {$difftext} {$nflag}{$mflag} {$link}{$userlink}{$comment} {$topmarktext}";
+ if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
+ $ret .= ' ' . wfMsgHtml( 'deletedrev' );
+ }
+ // Let extensions add data
+ wfRunHooks( 'ContributionsLineEnding', array( &$this, &$ret, $row ) );
+
+ $ret = "<li>$ret</li>\n";
+ wfProfileOut( __METHOD__ );
+ return $ret;
+ }
+
+ /**
+ * Get the Database object in use
+ *
+ * @return Database
+ */
+ public function getDatabase() {
+ return $this->mDb;
+ }
+
+}
+
+/**
+ * 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/SpecialDeadendpages.php b/includes/specials/SpecialDeadendpages.php
new file mode 100644
index 00000000..a8416c97
--- /dev/null
+++ b/includes/specials/SpecialDeadendpages.php
@@ -0,0 +1,62 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * @ingroup SpecialPage
+ */
+class DeadendPagesPage extends PageQueryPage {
+
+ function getName( ) {
+ return "Deadendpages";
+ }
+
+ function getPageHeader() {
+ return wfMsgExt( 'deadendpagestext', array( 'parse' ) );
+ }
+
+ /**
+ * LEFT JOIN is expensive
+ *
+ * @return true
+ */
+ function isExpensive( ) {
+ return 1;
+ }
+
+ function isSyndicated() { return false; }
+
+ /**
+ * @return false
+ */
+ function sortDescending() {
+ return false;
+ }
+
+ /**
+ * @return string an sqlquery
+ */
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ list( $page, $pagelinks ) = $dbr->tableNamesN( 'page', 'pagelinks' );
+ return "SELECT 'Deadendpages' as type, page_namespace AS namespace, page_title as title, page_title AS value " .
+ "FROM $page LEFT JOIN $pagelinks ON page_id = pl_from " .
+ "WHERE pl_from IS NULL " .
+ "AND page_namespace = 0 " .
+ "AND page_is_redirect = 0";
+ }
+}
+
+/**
+ * Constructor
+ */
+function wfSpecialDeadendpages() {
+
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $depp = new DeadendPagesPage();
+
+ return $depp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialDisambiguations.php b/includes/specials/SpecialDisambiguations.php
new file mode 100644
index 00000000..34045660
--- /dev/null
+++ b/includes/specials/SpecialDisambiguations.php
@@ -0,0 +1,108 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * @ingroup SpecialPage
+ */
+class DisambiguationsPage extends PageQueryPage {
+
+ function getName() {
+ return 'Disambiguations';
+ }
+
+ function isExpensive( ) { return true; }
+ function isSyndicated() { return false; }
+
+
+ function getPageHeader( ) {
+ return wfMsgExt( 'disambiguations-text', array( 'parse' ) );
+ }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+
+ $dMsgText = wfMsgForContent('disambiguationspage');
+
+ $linkBatch = new LinkBatch;
+
+ # If the text can be treated as a title, use it verbatim.
+ # Otherwise, pull the titles from the links table
+ $dp = Title::newFromText($dMsgText);
+ if( $dp ) {
+ if($dp->getNamespace() != NS_TEMPLATE) {
+ # FIXME we assume the disambiguation message is a template but
+ # the page can potentially be from another namespace :/
+ wfDebug("Mediawiki:disambiguationspage message does not refer to a template!\n");
+ }
+ $linkBatch->addObj( $dp );
+ } else {
+ # Get all the templates linked from the Mediawiki:Disambiguationspage
+ $disPageObj = Title::makeTitleSafe( NS_MEDIAWIKI, 'disambiguationspage' );
+ $res = $dbr->select(
+ array('pagelinks', 'page'),
+ 'pl_title',
+ array('page_id = pl_from', 'pl_namespace' => NS_TEMPLATE,
+ 'page_namespace' => $disPageObj->getNamespace(), 'page_title' => $disPageObj->getDBkey()),
+ __METHOD__ );
+
+ while ( $row = $dbr->fetchObject( $res ) ) {
+ $linkBatch->addObj( Title::makeTitle( NS_TEMPLATE, $row->pl_title ));
+ }
+
+ $dbr->freeResult( $res );
+ }
+
+ $set = $linkBatch->constructSet( 'lb.tl', $dbr );
+ if( $set === false ) {
+ # We must always return a valid sql query, but this way DB will always quicly return an empty result
+ $set = 'FALSE';
+ wfDebug("Mediawiki:disambiguationspage message does not link to any templates!\n");
+ }
+
+ list( $page, $pagelinks, $templatelinks) = $dbr->tableNamesN( 'page', 'pagelinks', 'templatelinks' );
+
+ $sql = "SELECT 'Disambiguations' AS \"type\", pb.page_namespace AS namespace,"
+ ." pb.page_title AS title, la.pl_from AS value"
+ ." FROM {$templatelinks} AS lb, {$page} AS pb, {$pagelinks} AS la, {$page} AS pa"
+ ." WHERE $set" # disambiguation template(s)
+ .' AND pa.page_id = la.pl_from'
+ .' AND pa.page_namespace = ' . NS_MAIN # Limit to just articles in the main namespace
+ .' AND pb.page_id = lb.tl_from'
+ .' AND pb.page_namespace = la.pl_namespace'
+ .' AND pb.page_title = la.pl_title'
+ .' ORDER BY lb.tl_namespace, lb.tl_title';
+
+ return $sql;
+ }
+
+ function getOrder() {
+ return '';
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgContLang;
+ $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' );
+ $arr = $wgContLang->getArrow();
+ $to = $skin->makeKnownLinkObj( $dp, '' );
+
+ return "$from $edit $arr $to";
+ }
+}
+
+/**
+ * Constructor
+ */
+function wfSpecialDisambiguations() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $sd = new DisambiguationsPage();
+
+ return $sd->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialDoubleRedirects.php b/includes/specials/SpecialDoubleRedirects.php
new file mode 100644
index 00000000..b1bad0c3
--- /dev/null
+++ b/includes/specials/SpecialDoubleRedirects.php
@@ -0,0 +1,103 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * A special page listing redirects to redirecting page.
+ * The software will automatically not follow double redirects, to prevent loops.
+ * @ingroup SpecialPage
+ */
+class DoubleRedirectsPage extends PageQueryPage {
+
+ function getName() {
+ return 'DoubleRedirects';
+ }
+
+ function isExpensive( ) { return true; }
+ function isSyndicated() { return false; }
+
+ function getPageHeader( ) {
+ return wfMsgExt( 'doubleredirectstext', array( 'parse' ) );
+ }
+
+ function getSQLText( &$dbr, $namespace = null, $title = null ) {
+
+ list( $page, $redirect ) = $dbr->tableNamesN( 'page', 'redirect' );
+
+ $limitToTitle = !( $namespace === null && $title === null );
+ $sql = $limitToTitle ? "SELECT" : "SELECT 'DoubleRedirects' as type," ;
+ $sql .=
+ " pa.page_namespace as namespace, pa.page_title as title," .
+ " pb.page_namespace as nsb, pb.page_title as tb," .
+ " pc.page_namespace as nsc, pc.page_title as tc" .
+ " FROM $redirect AS ra, $redirect AS rb, $page AS pa, $page AS pb, $page AS pc" .
+ " WHERE ra.rd_from=pa.page_id" .
+ " AND ra.rd_namespace=pb.page_namespace" .
+ " AND ra.rd_title=pb.page_title" .
+ " AND rb.rd_from=pb.page_id" .
+ " AND rb.rd_namespace=pc.page_namespace" .
+ " AND rb.rd_title=pc.page_title";
+
+ if( $limitToTitle ) {
+ $encTitle = $dbr->addQuotes( $title );
+ $sql .= " AND pa.page_namespace=$namespace" .
+ " AND pa.page_title=$encTitle";
+ }
+
+ return $sql;
+ }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ return $this->getSQLText( $dbr );
+ }
+
+ function getOrder() {
+ return '';
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgContLang;
+
+ $fname = 'DoubleRedirectsPage::formatResult';
+ $titleA = Title::makeTitle( $result->namespace, $result->title );
+
+ if ( $result && !isset( $result->nsb ) ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ $sql = $this->getSQLText( $dbr, $result->namespace, $result->title );
+ $res = $dbr->query( $sql, $fname );
+ if ( $res ) {
+ $result = $dbr->fetchObject( $res );
+ $dbr->freeResult( $res );
+ }
+ }
+ if ( !$result ) {
+ return '<s>' . $skin->makeLinkObj( $titleA, '', 'redirect=no' ) . '</s>';
+ }
+
+ $titleB = Title::makeTitle( $result->nsb, $result->tb );
+ $titleC = Title::makeTitle( $result->nsc, $result->tc );
+
+ $linkA = $skin->makeKnownLinkObj( $titleA, '', 'redirect=no' );
+ $edit = $skin->makeBrokenLinkObj( $titleA, "(".wfMsg("qbedit").")" , 'redirect=no');
+ $linkB = $skin->makeKnownLinkObj( $titleB, '', 'redirect=no' );
+ $linkC = $skin->makeKnownLinkObj( $titleC );
+ $arr = $wgContLang->getArrow() . $wgContLang->getDirMark();
+
+ return( "{$linkA} {$edit} {$arr} {$linkB} {$arr} {$linkC}" );
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialDoubleRedirects() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $sdr = new DoubleRedirectsPage();
+
+ return $sdr->doQuery( $offset, $limit );
+
+}
diff --git a/includes/specials/SpecialEmailuser.php b/includes/specials/SpecialEmailuser.php
new file mode 100644
index 00000000..3874c6a1
--- /dev/null
+++ b/includes/specials/SpecialEmailuser.php
@@ -0,0 +1,286 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * @todo document
+ */
+function wfSpecialEmailuser( $par ) {
+ global $wgRequest, $wgUser, $wgOut;
+
+ $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] );
+ return;
+ }
+
+ $form = new EmailUserForm( $targetUser,
+ $wgRequest->getText( 'wpText' ),
+ $wgRequest->getText( 'wpSubject' ),
+ $wgRequest->getBool( 'wpCCMe' ) );
+ if ( $action == 'success' ) {
+ $form->showSuccess();
+ return;
+ }
+
+ $error = EmailUserForm::getPermissionsError( $wgUser, $wgRequest->getVal( 'wpEditToken' ) );
+ if ( $error ) {
+ switch ( $error[0] ) {
+ case 'blockedemailuser':
+ $wgOut->blockedPage();
+ return;
+ case 'actionthrottledtext':
+ $wgOut->rateLimited();
+ return;
+ case 'sessionfailure':
+ $form->showForm();
+ return;
+ default:
+ $wgOut->showErrorPage( $error[0], $error[1] );
+ return;
+ }
+ }
+
+
+ if ( "submit" == $action && $wgRequest->wasPosted() ) {
+ $result = $form->doSubmit();
+
+ if ( !is_null( $result ) ) {
+ $wgOut->addHTML( wfMsg( "usermailererror" ) .
+ ' ' . htmlspecialchars( $result->getMessage() ) );
+ } else {
+ $titleObj = SpecialPage::getTitleFor( "Emailuser" );
+ $encTarget = wfUrlencode( $form->getTarget()->getName() );
+ $wgOut->redirect( $titleObj->getFullURL( "target={$encTarget}&action=success" ) );
+ }
+ } else {
+ $form->showForm();
+ }
+}
+
+/**
+ * Implements the Special:Emailuser web interface, and invokes userMailer for sending the email message.
+ * @ingroup SpecialPage
+ */
+class EmailUserForm {
+
+ var $target;
+ var $text, $subject;
+ var $cc_me; // Whether user requested to be sent a separate copy of their email.
+
+ /**
+ * @param User $target
+ */
+ function EmailUserForm( $target, $text, $subject, $cc_me ) {
+ $this->target = $target;
+ $this->text = $text;
+ $this->subject = $subject;
+ $this->cc_me = $cc_me;
+ }
+
+ function showForm() {
+ global $wgOut, $wgUser;
+ $skin = $wgUser->getSkin();
+
+ $wgOut->setPagetitle( wfMsg( "emailpage" ) );
+ $wgOut->addWikiMsg( "emailpagetext" );
+
+ if ( $this->subject === "" ) {
+ $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=" .
+ 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" );
+
+ }
+
+ /*
+ * Really send a mail. Permissions should have been checked using
+ * EmailUserForm::getPermissionsError. It is probably also a good idea to
+ * check the edit token and ping limiter in advance.
+ */
+ function doSubmit() {
+ global $wgUser, $wgUserEmailUseReplyTo, $wgSiteName;
+
+ $to = new MailAddress( $this->target );
+ $from = new MailAddress( $wgUser );
+ $subject = $this->subject;
+
+ // Add a standard footer and trim up trailing newlines
+ $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 ) ) ) {
+
+ if( $wgUserEmailUseReplyTo ) {
+ // Put the generic wiki autogenerated address in the From:
+ // header and reserve the user for Reply-To.
+ //
+ // This is a bit ugly, but will serve to differentiate
+ // wiki-borne mails from direct mails and protects against
+ // SPF and bounce problems with some mailers (see below).
+ global $wgPasswordSender;
+ $mailFrom = new MailAddress( $wgPasswordSender );
+ $replyTo = $from;
+ } else {
+ // Put the sending user's e-mail address in the From: header.
+ //
+ // This is clean-looking and convenient, but has issues.
+ // One is that it doesn't as clearly differentiate the wiki mail
+ // from "directly" sent mails.
+ //
+ // Another is that some mailers (like sSMTP) will use the From
+ // address as the envelope sender as well. For open sites this
+ // can cause mails to be flunked for SPF violations (since the
+ // wiki server isn't an authorized sender for various users'
+ // domains) as well as creating a privacy issue as bounces
+ // containing the recipient's e-mail address may get sent to
+ // the sending user.
+ $mailFrom = $from;
+ $replyTo = null;
+ }
+
+ $mailResult = UserMailer::send( $to, $mailFrom, $subject, $this->text, $replyTo );
+
+ if( WikiError::isError( $mailResult ) ) {
+ return $mailResult;
+
+ } else {
+
+ // if the user requested a copy of this mail, do this now,
+ // unless they are emailing themselves, in which case one copy of the message is sufficient.
+ if ($this->cc_me && $to != $from) {
+ $cc_subject = wfMsg('emailccsubject', $this->target->getName(), $subject);
+ if( wfRunHooks( 'EmailUser', array( &$from, &$from, &$cc_subject, &$this->text ) ) ) {
+ $ccResult = UserMailer::send( $from, $from, $cc_subject, $this->text );
+ if( WikiError::isError( $ccResult ) ) {
+ // At this stage, the user's CC mail has failed, but their
+ // original mail has succeeded. It's unlikely, but still, what to do?
+ // We can either show them an error, or we can say everything was fine,
+ // or we can say we sort of failed AND sort of succeeded. Of these options,
+ // simply saying there was an error is probably best.
+ return $ccResult;
+ }
+ }
+ }
+
+ wfRunHooks( 'EmailUserComplete', array( $to, $from, $subject, $this->text ) );
+ return;
+ }
+ }
+ }
+
+ function showSuccess( &$user = null ) {
+ global $wgOut;
+
+ if ( is_null($user) )
+ $user = $this->target;
+
+ $wgOut->setPagetitle( wfMsg( "emailsent" ) );
+ $wgOut->addHTML( wfMsg( "emailsenttext" ) );
+
+ $wgOut->returnToMain( false, $user->getUserPage() );
+ }
+
+ function getTarget() {
+ return $this->target;
+ }
+
+ static function validateEmailTarget ( $target ) {
+ global $wgEnableEmail, $wgEnableUserEmail;
+
+ if( !( $wgEnableEmail && $wgEnableUserEmail ) )
+ return array( "nosuchspecialpage", "nospecialpagetext" );
+
+ if ( "" == $target ) {
+ wfDebug( "Target is empty.\n" );
+ return array( "notargettitle", "notargettext" );
+ }
+
+ $nt = Title::newFromURL( $target );
+ if ( is_null( $nt ) ) {
+ wfDebug( "Target is invalid title.\n" );
+ return array( "notargettitle", "notargettext" );
+ }
+
+ $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" );
+ }
+
+ return $nu;
+ }
+ static function getPermissionsError ( $user, $editToken ) {
+ if( !$user->canSendEmail() ) {
+ wfDebug( "User can't send.\n" );
+ return array( "mailnologin", "mailnologintext" );
+ }
+
+ if( $user->isBlockedFromEmailuser() ) {
+ wfDebug( "User is blocked from sending e-mail.\n" );
+ return array( "blockedemailuser", "" );
+ }
+
+ if( $user->pingLimiter( 'emailuser' ) ) {
+ wfDebug( "Ping limiter triggered.\n" );
+ return array( 'actionthrottledtext', '' );
+ }
+
+ if( !$user->matchEditToken( $editToken ) ) {
+ wfDebug( "Matching edit token failed.\n" );
+ return array( 'sessionfailure', '' );
+ }
+
+ return;
+ }
+
+ static function newFromURL( $target, $text, $subject, $cc_me )
+ {
+ $nt = Title::newFromURL( $target );
+ $nu = User::newFromName( $nt->getText() );
+ return new EmailUserForm( $nu, $text, $subject, $cc_me );
+ }
+}
diff --git a/includes/specials/SpecialExport.php b/includes/specials/SpecialExport.php
new file mode 100644
index 00000000..38bfc83e
--- /dev/null
+++ b/includes/specials/SpecialExport.php
@@ -0,0 +1,284 @@
+<?php
+# Copyright (C) 2003 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
+ */
+
+function wfExportGetPagesFromCategory( $title ) {
+ global $wgContLang;
+
+ $name = $title->getDBkey();
+
+ $dbr = wfGetDB( DB_SLAVE );
+
+ list( $page, $categorylinks ) = $dbr->tableNamesN( 'page', 'categorylinks' );
+ $sql = "SELECT page_namespace, page_title FROM $page " .
+ "JOIN $categorylinks ON cl_from = page_id " .
+ "WHERE cl_to = " . $dbr->addQuotes( $name );
+
+ $pages = array();
+ $res = $dbr->query( $sql, 'wfExportGetPagesFromCategory' );
+ while ( $row = $dbr->fetchObject( $res ) ) {
+ $n = $row->page_title;
+ if ($row->page_namespace) {
+ $ns = $wgContLang->getNsText( $row->page_namespace );
+ $n = $ns . ':' . $n;
+ }
+
+ $pages[] = $n;
+ }
+ $dbr->freeResult($res);
+
+ return $pages;
+}
+
+/**
+ * Expand a list of pages to include templates used in those pages.
+ * @param $inputPages array, list of titles to look up
+ * @param $pageSet array, associative array indexed by titles for output
+ * @return array associative array index by titles
+ */
+function wfExportGetTemplates( $inputPages, $pageSet ) {
+ return wfExportGetLinks( $inputPages, $pageSet,
+ 'templatelinks',
+ array( 'tl_namespace AS namespace', 'tl_title AS title' ),
+ array( 'page_id=tl_from' ) );
+}
+
+/**
+ * Expand a list of pages to include images used in those pages.
+ * @param $inputPages array, list of titles to look up
+ * @param $pageSet array, associative array indexed by titles for output
+ * @return array associative array index by titles
+ */
+function wfExportGetImages( $inputPages, $pageSet ) {
+ return wfExportGetLinks( $inputPages, $pageSet,
+ 'imagelinks',
+ array( NS_IMAGE . ' AS namespace', 'il_to AS title' ),
+ array( 'page_id=il_from' ) );
+}
+
+/**
+ * Expand a list of pages to include items used in those pages.
+ * @private
+ */
+function wfExportGetLinks( $inputPages, $pageSet, $table, $fields, $join ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ foreach( $inputPages as $page ) {
+ $title = Title::newFromText( $page );
+ if( $title ) {
+ $pageSet[$title->getPrefixedText()] = true;
+ /// @fixme May or may not be more efficient to batch these
+ /// by namespace when given multiple input pages.
+ $result = $dbr->select(
+ array( 'page', $table ),
+ $fields,
+ array_merge( $join,
+ array(
+ 'page_namespace' => $title->getNamespace(),
+ 'page_title' => $title->getDbKey() ) ),
+ __METHOD__ );
+ foreach( $result as $row ) {
+ $template = Title::makeTitle( $row->namespace, $row->title );
+ $pageSet[$template->getPrefixedText()] = true;
+ }
+ }
+ }
+ return $pageSet;
+}
+
+/**
+ * Callback function to remove empty strings from the pages array.
+ */
+function wfFilterPage( $page ) {
+ return $page !== '' && $page !== null;
+}
+
+/**
+ *
+ */
+function wfSpecialExport( $page = '' ) {
+ global $wgOut, $wgRequest, $wgSitename, $wgExportAllowListContributors;
+ global $wgExportAllowHistory, $wgExportMaxHistory;
+
+ $curonly = true;
+ $doexport = false;
+
+ if ( $wgRequest->getCheck( 'addcat' ) ) {
+ $page = $wgRequest->getText( 'pages' );
+ $catname = $wgRequest->getText( 'catname' );
+
+ if ( $catname !== '' && $catname !== NULL && $catname !== false ) {
+ $t = Title::makeTitleSafe( NS_CATEGORY, $catname );
+ if ( $t ) {
+ /**
+ * @fixme This can lead to hitting memory limit for very large
+ * categories. Ideally we would do the lookup synchronously
+ * during the export in a single query.
+ */
+ $catpages = wfExportGetPagesFromCategory( $t );
+ if ( $catpages ) $page .= "\n" . implode( "\n", $catpages );
+ }
+ }
+ }
+ else if( $wgRequest->wasPosted() && $page == '' ) {
+ $page = $wgRequest->getText( 'pages' );
+ $curonly = $wgRequest->getCheck( 'curonly' );
+ $rawOffset = $wgRequest->getVal( 'offset' );
+ if( $rawOffset ) {
+ $offset = wfTimestamp( TS_MW, $rawOffset );
+ } else {
+ $offset = null;
+ }
+ $limit = $wgRequest->getInt( 'limit' );
+ $dir = $wgRequest->getVal( 'dir' );
+ $history = array(
+ 'dir' => 'asc',
+ 'offset' => false,
+ 'limit' => $wgExportMaxHistory,
+ );
+ $historyCheck = $wgRequest->getCheck( 'history' );
+ if ( $curonly ) {
+ $history = WikiExporter::CURRENT;
+ } elseif ( !$historyCheck ) {
+ if ( $limit > 0 && $limit < $wgExportMaxHistory ) {
+ $history['limit'] = $limit;
+ }
+ if ( !is_null( $offset ) ) {
+ $history['offset'] = $offset;
+ }
+ if ( strtolower( $dir ) == 'desc' ) {
+ $history['dir'] = 'desc';
+ }
+ }
+
+ if( $page != '' ) $doexport = true;
+ } else {
+ // Default to current-only for GET requests
+ $page = $wgRequest->getText( 'pages', $page );
+ $historyCheck = $wgRequest->getCheck( 'history' );
+ if( $historyCheck ) {
+ $history = WikiExporter::FULL;
+ } else {
+ $history = WikiExporter::CURRENT;
+ }
+
+ if( $page != '' ) $doexport = true;
+ }
+
+ if( !$wgExportAllowHistory ) {
+ // Override
+ $history = WikiExporter::CURRENT;
+ }
+
+ $list_authors = $wgRequest->getCheck( 'listauthors' );
+ if ( !$curonly || !$wgExportAllowListContributors ) $list_authors = false ;
+
+ if ( $doexport ) {
+ $wgOut->disable();
+
+ // Cancel output buffering and gzipping if set
+ // This should provide safer streaming for pages with history
+ wfResetOutputBuffers();
+ header( "Content-type: application/xml; charset=utf-8" );
+ if( $wgRequest->getCheck( 'wpDownload' ) ) {
+ // Provide a sane filename suggestion
+ $filename = urlencode( $wgSitename . '-' . wfTimestampNow() . '.xml' );
+ $wgRequest->response()->header( "Content-disposition: attachment;filename={$filename}" );
+ }
+
+ /* Split up the input and look up linked pages */
+ $inputPages = array_filter( explode( "\n", $page ), 'wfFilterPage' );
+ $pageSet = array_flip( $inputPages );
+
+ if( $wgRequest->getCheck( 'templates' ) ) {
+ $pageSet = wfExportGetTemplates( $inputPages, $pageSet );
+ }
+
+ /*
+ // Enable this when we can do something useful exporting/importing image information. :)
+ if( $wgRequest->getCheck( 'images' ) ) {
+ $pageSet = wfExportGetImages( $inputPages, $pageSet );
+ }
+ */
+
+ $pages = array_keys( $pageSet );
+
+ /* Ok, let's get to it... */
+
+ $db = wfGetDB( DB_SLAVE );
+ $exporter = new WikiExporter( $db, $history );
+ $exporter->list_authors = $list_authors ;
+ $exporter->openStream();
+
+ foreach( $pages as $page ) {
+ /*
+ if( $wgExportMaxHistory && !$curonly ) {
+ $title = Title::newFromText( $page );
+ if( $title ) {
+ $count = Revision::countByTitle( $db, $title );
+ if( $count > $wgExportMaxHistory ) {
+ wfDebug( __FUNCTION__ .
+ ": Skipped $page, $count revisions too big\n" );
+ continue;
+ }
+ }
+ }*/
+
+ #Bug 8824: Only export pages the user can read
+ $title = Title::newFromText( $page );
+ if( is_null( $title ) ) continue; #TODO: perhaps output an <error> tag or something.
+ if( !$title->userCanRead() ) continue; #TODO: perhaps output an <error> tag or something.
+
+ $exporter->pageByTitle( $title );
+ }
+
+ $exporter->closeStream();
+ return;
+ }
+
+ $self = SpecialPage::getTitleFor( 'Export' );
+ $wgOut->addHtml( wfMsgExt( 'exporttext', 'parse' ) );
+
+ $form = Xml::openElement( 'form', array( 'method' => 'post',
+ 'action' => $self->getLocalUrl( 'action=submit' ) ) );
+
+ $form .= Xml::inputLabel( wfMsg( 'export-addcattext' ) , 'catname', 'catname', 40 ) . '&nbsp;';
+ $form .= Xml::submitButton( wfMsg( 'export-addcat' ), array( 'name' => 'addcat' ) ) . '<br />';
+
+ $form .= Xml::openElement( 'textarea', array( 'name' => 'pages', 'cols' => 40, 'rows' => 10 ) );
+ $form .= htmlspecialchars( $page );
+ $form .= Xml::closeElement( 'textarea' );
+ $form .= '<br />';
+
+ if( $wgExportAllowHistory ) {
+ $form .= Xml::checkLabel( wfMsg( 'exportcuronly' ), 'curonly', 'curonly', true ) . '<br />';
+ } else {
+ $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::closeElement( 'form' );
+ $wgOut->addHtml( $form );
+}
diff --git a/includes/specials/SpecialFewestrevisions.php b/includes/specials/SpecialFewestrevisions.php
new file mode 100644
index 00000000..afd5ad48
--- /dev/null
+++ b/includes/specials/SpecialFewestrevisions.php
@@ -0,0 +1,74 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Special page for listing the articles with the fewest revisions.
+ *
+ * @ingroup SpecialPage
+ * @author Martin Drashkov
+ */
+class FewestrevisionsPage extends QueryPage {
+
+ function getName() {
+ return 'Fewestrevisions';
+ }
+
+ function isExpensive() {
+ return true;
+ }
+
+ function isSyndicated() {
+ return false;
+ }
+
+ function getSql() {
+ $dbr = wfGetDB( DB_SLAVE );
+ list( $revision, $page ) = $dbr->tableNamesN( 'revision', 'page' );
+
+ return "SELECT 'Fewestrevisions' as type,
+ page_namespace as namespace,
+ page_title as title,
+ page_is_redirect as redirect,
+ COUNT(*) as value
+ FROM $revision
+ JOIN $page ON page_id = rev_page
+ WHERE page_namespace = " . NS_MAIN . "
+ GROUP BY page_namespace, page_title, page_is_redirect
+ HAVING COUNT(*) > 1";
+ // ^^^ This was probably here to weed out redirects.
+ // Since we mark them as such now, it might be
+ // useful to remove this. People _do_ create pages
+ // and never revise them, they aren't necessarily
+ // redirects.
+ }
+
+ function sortDescending() {
+ return false;
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgLang, $wgContLang;
+
+ $nt = Title::makeTitleSafe( $result->namespace, $result->title );
+ $text = $wgContLang->convert( $nt->getPrefixedText() );
+
+ $plink = $skin->makeKnownLinkObj( $nt, $text );
+
+ $nl = wfMsgExt( 'nrevisions', array( 'parsemag', 'escape'),
+ $wgLang->formatNum( $result->value ) );
+ $redirect = $result->redirect ? ' - ' . wfMsg( 'isredirect' ) : '';
+ $nlink = $skin->makeKnownLinkObj( $nt, $nl, 'action=history' ) . $redirect;
+
+
+ return wfSpecialList( $plink, $nlink );
+ }
+}
+
+function wfSpecialFewestrevisions() {
+ list( $limit, $offset ) = wfCheckLimits();
+ $frp = new FewestrevisionsPage();
+ $frp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialFileDuplicateSearch.php b/includes/specials/SpecialFileDuplicateSearch.php
new file mode 100644
index 00000000..5236ca25
--- /dev/null
+++ b/includes/specials/SpecialFileDuplicateSearch.php
@@ -0,0 +1,135 @@
+<?php
+/**
+ * A special page to search for files by hash value as defined in the
+ * img_sha1 field in the image table
+ *
+ * @file
+ * @ingroup SpecialPage
+ *
+ * @author Raimond Spekking, based on Special:MIMESearch by Ævar Arnfjörð Bjarmason
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ */
+
+/**
+ * Searches the database for files of the requested hash, comparing this with the
+ * 'img_sha1' field in the image table.
+ * @ingroup SpecialPage
+ */
+class FileDuplicateSearchPage extends QueryPage {
+ var $hash, $filename;
+
+ function FileDuplicateSearchPage( $hash, $filename ) {
+ $this->hash = $hash;
+ $this->filename = $filename;
+ }
+
+ function getName() { return 'FileDuplicateSearch'; }
+ function isExpensive() { return false; }
+ function isSyndicated() { return false; }
+
+ function linkParameters() {
+ return array( 'filename' => $this->filename );
+ }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ $image = $dbr->tableName( 'image' );
+ $hash = $dbr->addQuotes( $this->hash );
+
+ return "SELECT 'FileDuplicateSearch' AS type,
+ img_name AS title,
+ img_sha1 AS value,
+ img_user_text,
+ img_timestamp
+ FROM $image
+ WHERE img_sha1 = $hash
+ ";
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgContLang, $wgLang;
+
+ $nt = Title::makeTitle( NS_IMAGE, $result->title );
+ $text = $wgContLang->convert( $nt->getText() );
+ $plink = $skin->makeLink( $nt->getPrefixedText(), $text );
+
+ $user = $skin->makeLinkObj( Title::makeTitle( NS_USER, $result->img_user_text ), $result->img_user_text );
+ $time = $wgLang->timeanddate( $result->img_timestamp );
+
+ return "$plink . . $user . . $time";
+ }
+}
+
+/**
+ * Output the HTML search form, and constructs the FileDuplicateSearch object.
+ */
+function wfSpecialFileDuplicateSearch( $par = null ) {
+ global $wgRequest, $wgTitle, $wgOut, $wgLang, $wgContLang;
+
+ $hash = '';
+ $filename = isset( $par ) ? $par : $wgRequest->getText( 'filename' );
+
+ $title = Title::newFromText( $filename );
+ if( $title && $title->getText() != '' ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ $image = $dbr->tableName( 'image' );
+ $encFilename = $dbr->addQuotes( htmlspecialchars( $title->getDbKey() ) );
+ $sql = "SELECT img_sha1 from $image where img_name = $encFilename";
+ $res = $dbr->query( $sql );
+ $row = $dbr->fetchRow( $res );
+ if( $row !== false ) {
+ $hash = $row[0];
+ }
+ $dbr->freeResult( $res );
+ }
+
+ # Create the input form
+ $wgOut->addHTML(
+ Xml::openElement( 'form', array( 'id' => 'fileduplicatesearch', 'method' => 'get', 'action' => $wgTitle->getLocalUrl() ) ) .
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', null, wfMsg( 'fileduplicatesearch-legend' ) ) .
+ Xml::inputLabel( wfMsg( 'fileduplicatesearch-filename' ), 'filename', 'filename', 50, $filename ) . ' ' .
+ Xml::submitButton( wfMsg( 'fileduplicatesearch-submit' ) ) .
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' )
+ );
+
+ if( $hash != '' ) {
+ $align = $wgContLang->isRtl() ? 'left' : 'right';
+
+ # Show a thumbnail of the file
+ $img = wfFindFile( $title );
+ if ( $img ) {
+ $thumb = $img->getThumbnail( 120, 120 );
+ if( $thumb ) {
+ $wgOut->addHTML( '<div style="float:' . $align . '" id="mw-fileduplicatesearch-icon">' .
+ $thumb->toHtml( array( 'desc-link' => false ) ) . '<br />' .
+ wfMsgExt( 'fileduplicatesearch-info', array( 'parse' ),
+ $wgLang->formatNum( $img->getWidth() ),
+ $wgLang->formatNum( $img->getHeight() ),
+ $wgLang->formatSize( $img->getSize() ),
+ $img->getMimeType()
+ ) .
+ '</div>' );
+ }
+ }
+
+ # Do the query
+ $wpp = new FileDuplicateSearchPage( $hash, $filename );
+ list( $limit, $offset ) = wfCheckLimits();
+ $count = $wpp->doQuery( $offset, $limit );
+
+ # Show a short summary
+ if( $count == 1 ) {
+ $wgOut->addHTML( '<p class="mw-fileduplicatesearch-result-1">' .
+ wfMsgHtml( 'fileduplicatesearch-result-1', $filename ) .
+ '</p>'
+ );
+ } elseif ( $count > 1 ) {
+ $wgOut->addHTML( '<p class="mw-fileduplicatesearch-result-n">' .
+ wfMsgExt( 'fileduplicatesearch-result-n', array( 'parseinline' ), $filename, $wgLang->formatNum( $count - 1 ) ) .
+ '</p>'
+ );
+ }
+ }
+}
diff --git a/includes/specials/SpecialFilepath.php b/includes/specials/SpecialFilepath.php
new file mode 100644
index 00000000..a2ba3e57
--- /dev/null
+++ b/includes/specials/SpecialFilepath.php
@@ -0,0 +1,53 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+function wfSpecialFilepath( $par ) {
+ global $wgRequest, $wgOut;
+
+ $file = isset( $par ) ? $par : $wgRequest->getText( 'file' );
+
+ $title = Title::newFromText( $file, NS_IMAGE );
+
+ if ( ! $title instanceof Title || $title->getNamespace() != NS_IMAGE ) {
+ $cform = new FilepathForm( $title );
+ $cform->execute();
+ } else {
+ $file = wfFindFile( $title );
+ if ( $file && $file->exists() ) {
+ $wgOut->redirect( $file->getURL() );
+ } else {
+ $wgOut->setStatusCode( 404 );
+ $cform = new FilepathForm( $title );
+ $cform->execute();
+ }
+ }
+}
+
+/**
+ * @ingroup SpecialPage
+ */
+class FilepathForm {
+ var $mTitle;
+
+ function FilepathForm( &$title ) {
+ $this->mTitle =& $title;
+ }
+
+ function execute() {
+ global $wgOut, $wgTitle, $wgScript;
+
+ $wgOut->addHTML(
+ Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'id' => 'specialfilepath' ) ) .
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', null, wfMsg( 'filepath' ) ) .
+ Xml::hidden( 'title', $wgTitle->getPrefixedText() ) .
+ Xml::inputLabel( wfMsg( 'filepath-page' ), 'file', 'file', 25, is_object( $this->mTitle ) ? $this->mTitle->getText() : '' ) . ' ' .
+ Xml::submitButton( wfMsg( 'filepath-submit' ) ) . "\n" .
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' )
+ );
+ }
+}
diff --git a/includes/specials/SpecialImagelist.php b/includes/specials/SpecialImagelist.php
new file mode 100644
index 00000000..3d449b54
--- /dev/null
+++ b/includes/specials/SpecialImagelist.php
@@ -0,0 +1,161 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ *
+ */
+function wfSpecialImagelist() {
+ global $wgOut;
+
+ $pager = new ImageListPager;
+
+ $limit = $pager->getForm();
+ $body = $pager->getBody();
+ $nav = $pager->getNavigationBar();
+ $wgOut->addHTML( "$limit<br />\n$body<br />\n$nav" );
+}
+
+/**
+ * @ingroup SpecialPage Pager
+ */
+class ImageListPager extends TablePager {
+ var $mFieldNames = null;
+ var $mQueryConds = array();
+
+ function __construct() {
+ global $wgRequest, $wgMiserMode;
+ if ( $wgRequest->getText( 'sort', 'img_date' ) == 'img_date' ) {
+ $this->mDefaultDirection = true;
+ } else {
+ $this->mDefaultDirection = false;
+ }
+ $search = $wgRequest->getText( 'ilsearch' );
+ if ( $search != '' && !$wgMiserMode ) {
+ $nt = Title::newFromUrl( $search );
+ if( $nt ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ $m = $dbr->strencode( strtolower( $nt->getDBkey() ) );
+ $m = str_replace( "%", "\\%", $m );
+ $m = str_replace( "_", "\\_", $m );
+ $this->mQueryConds = array( "LOWER(img_name) LIKE '%{$m}%'" );
+ }
+ }
+
+ parent::__construct();
+ }
+
+ function getFieldNames() {
+ if ( !$this->mFieldNames ) {
+ $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' ),
+ );
+ }
+ return $this->mFieldNames;
+ }
+
+ function isFieldSortable( $field ) {
+ static $sortable = array( 'img_timestamp', 'img_name', 'img_size' );
+ return in_array( $field, $sortable );
+ }
+
+ function getQueryInfo() {
+ $fields = $this->getFieldNames();
+ $fields = array_keys( $fields );
+ $fields[] = 'img_user';
+ return array(
+ 'tables' => 'image',
+ 'fields' => $fields,
+ 'conds' => $this->mQueryConds
+ );
+ }
+
+ function getDefaultSort() {
+ return 'img_timestamp';
+ }
+
+ function getStartBody() {
+ # Do a link batch query for user pages
+ if ( $this->mResult->numRows() ) {
+ $lb = new LinkBatch;
+ $this->mResult->seek( 0 );
+ while ( $row = $this->mResult->fetchObject() ) {
+ if ( $row->img_user ) {
+ $lb->add( NS_USER, str_replace( ' ', '_', $row->img_user_text ) );
+ }
+ }
+ $lb->execute();
+ }
+
+ return parent::getStartBody();
+ }
+
+ function formatValue( $field, $value ) {
+ global $wgLang;
+ switch ( $field ) {
+ case 'img_timestamp':
+ return $wgLang->timeanddate( $value, true );
+ case 'img_name':
+ static $imgfile = null;
+ if ( $imgfile === null ) $imgfile = wfMsg( 'imgfile' );
+
+ $name = $this->mCurrentRow->img_name;
+ $link = $this->getSkin()->makeKnownLinkObj( Title::makeTitle( NS_IMAGE, $name ), $value );
+ $image = wfLocalFile( $value );
+ $url = $image->getURL();
+ $download = Xml::element('a', array( 'href' => $url ), $imgfile );
+ return "$link ($download)";
+ case 'img_user_text':
+ if ( $this->mCurrentRow->img_user ) {
+ $link = $this->getSkin()->makeLinkObj( Title::makeTitle( NS_USER, $value ),
+ htmlspecialchars( $value ) );
+ } else {
+ $link = htmlspecialchars( $value );
+ }
+ return $link;
+ case 'img_size':
+ return $this->getSkin()->formatSize( $value );
+ case 'img_description':
+ return $this->getSkin()->commentBlock( $value );
+ }
+ }
+
+ function getForm() {
+ global $wgRequest, $wgMiserMode;
+ $search = $wgRequest->getText( 'ilsearch' );
+
+ $s = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $this->getTitle()->getLocalURL(), 'id' => 'mw-imagelist-form' ) ) .
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', null, wfMsg( 'imagelist' ) ) .
+ 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 );
+ }
+ $s .= ' ' .
+ Xml::submitButton( wfMsg( 'table_pager_limit_submit' ) ) ."\n" .
+ $this->getHiddenFields( array( 'limit', 'ilsearch' ) ) .
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' ) . "\n";
+ return $s;
+ }
+
+ function getTableClass() {
+ return 'imagelist ' . parent::getTableClass();
+ }
+
+ function getNavClass() {
+ return 'imagelist_nav ' . parent::getNavClass();
+ }
+
+ function getSortHeaderClass() {
+ return 'imagelist_sort ' . parent::getSortHeaderClass();
+ }
+}
diff --git a/includes/specials/SpecialImport.php b/includes/specials/SpecialImport.php
new file mode 100644
index 00000000..4c37f1f9
--- /dev/null
+++ b/includes/specials/SpecialImport.php
@@ -0,0 +1,1154 @@
+<?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
+ */
+
+/**
+ * Constructor
+ */
+function wfSpecialImport( $page = '' ) {
+ global $wgUser, $wgOut, $wgRequest, $wgTitle, $wgImportSources;
+ global $wgImportTargetNamespace;
+
+ $interwiki = false;
+ $namespace = $wgImportTargetNamespace;
+ $frompage = '';
+ $history = true;
+
+ if ( wfReadOnly() ) {
+ $wgOut->readOnlyPage();
+ return;
+ }
+
+ if( $wgRequest->wasPosted() && $wgRequest->getVal( 'action' ) == 'submit') {
+ $isUpload = false;
+ $namespace = $wgRequest->getIntOrNull( 'namespace' );
+
+ switch( $wgRequest->getVal( "source" ) ) {
+ case "upload":
+ $isUpload = true;
+ if( $wgUser->isAllowed( 'importupload' ) ) {
+ $source = ImportStreamSource::newFromUpload( "xmlimport" );
+ } else {
+ return $wgOut->permissionRequired( 'importupload' );
+ }
+ break;
+ case "interwiki":
+ $interwiki = $wgRequest->getVal( 'interwiki' );
+ $history = $wgRequest->getCheck( 'interwikiHistory' );
+ $frompage = $wgRequest->getText( "frompage" );
+ $source = ImportStreamSource::newFromInterwiki(
+ $interwiki,
+ $frompage,
+ $history );
+ break;
+ default:
+ $source = new WikiErrorMsg( "importunknownsource" );
+ }
+
+ if( WikiError::isError( $source ) ) {
+ $wgOut->wrapWikiMsg( '<p class="error">$1</p>', array( 'importfailed', $source->getMessage() ) );
+ } else {
+ $wgOut->addWikiMsg( "importstart" );
+
+ $importer = new WikiImporter( $source );
+ if( !is_null( $namespace ) ) {
+ $importer->setTargetNamespace( $namespace );
+ }
+ $reporter = new ImportReporter( $importer, $isUpload, $interwiki );
+
+ $reporter->open();
+ $result = $importer->doImport();
+ $resultCount = $reporter->close();
+
+ if( WikiError::isError( $result ) ) {
+ # No source or XML parse error
+ $wgOut->wrapWikiMsg( '<p class="error">$1</p>', array( 'importfailed', $result->getMessage() ) );
+ } elseif( WikiError::isError( $resultCount ) ) {
+ # Zero revisions
+ $wgOut->wrapWikiMsg( '<p class="error">$1</p>', array( 'importfailed', $resultCount->getMessage() ) );
+ } else {
+ # Success!
+ $wgOut->addWikiMsg( 'importsuccess' );
+ }
+ $wgOut->addWikiText( '<hr />' );
+ }
+ }
+
+ $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::submitButton( wfMsg( 'uploadbtn' ) ) .
+ 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::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 ) );
+ }
+ $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' )
+ );
+ }
+}
+
+/**
+ * Reporting callback
+ * @ingroup SpecialPage
+ */
+class ImportReporter {
+ function __construct( $importer, $upload, $interwiki ) {
+ $importer->setPageOutCallback( array( $this, 'reportPage' ) );
+ $this->mPageCount = 0;
+ $this->mIsUpload = $upload;
+ $this->mInterwiki = $interwiki;
+ }
+
+ function open() {
+ global $wgOut;
+ $wgOut->addHtml( "<ul>\n" );
+ }
+
+ function reportPage( $title, $origTitle, $revisionCount, $successCount ) {
+ global $wgOut, $wgUser, $wgLang, $wgContLang;
+
+ $skin = $wgUser->getSkin();
+
+ $this->mPageCount++;
+
+ $localCount = $wgLang->formatNum( $successCount );
+ $contentCount = $wgContLang->formatNum( $successCount );
+
+ if( $successCount > 0 ) {
+ $wgOut->addHtml( "<li>" . $skin->makeKnownLinkObj( $title ) . " " .
+ wfMsgExt( 'import-revision-count', array( 'parsemag', 'escape' ), $localCount ) .
+ "</li>\n"
+ );
+
+ $log = new LogPage( 'import' );
+ if( $this->mIsUpload ) {
+ $detail = wfMsgExt( 'import-logentry-upload-detail', array( 'content', 'parsemag' ),
+ $contentCount );
+ $log->addEntry( 'upload', $title, $detail );
+ } else {
+ $interwiki = '[[:' . $this->mInterwiki . ':' .
+ $origTitle->getPrefixedText() . ']]';
+ $detail = wfMsgExt( 'import-logentry-interwiki-detail', array( 'content', 'parsemag' ),
+ $contentCount, $interwiki );
+ $log->addEntry( 'interwiki', $title, $detail );
+ }
+
+ $comment = $detail; // quick
+ $dbw = wfGetDB( DB_MASTER );
+ $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) );
+ } else {
+ $wgOut->addHtml( '<li>' . wfMsgHtml( 'import-nonewrevisions' ) . '</li>' );
+ }
+ }
+
+ function close() {
+ global $wgOut;
+ if( $this->mPageCount == 0 ) {
+ $wgOut->addHtml( "</ul>\n" );
+ return new WikiErrorMsg( "importnopages" );
+ }
+ $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
new file mode 100644
index 00000000..696c7efe
--- /dev/null
+++ b/includes/specials/SpecialIpblocklist.php
@@ -0,0 +1,427 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * @todo document
+ */
+function wfSpecialIpblocklist() {
+ global $wgUser, $wgOut, $wgRequest;
+
+ $ip = $wgRequest->getVal( 'wpUnblockAddress', $wgRequest->getVal( 'ip' ) );
+ $id = $wgRequest->getVal( 'id' );
+ $reason = $wgRequest->getText( 'wpUnblockReason' );
+ $action = $wgRequest->getText( 'action' );
+ $successip = $wgRequest->getVal( 'successip' );
+
+ $ipu = new IPUnblockForm( $ip, $id, $reason );
+
+ if( $action == 'unblock' ) {
+ # Check permissions
+ if( !$wgUser->isAllowed( 'block' ) ) {
+ $wgOut->permissionRequired( 'block' );
+ return;
+ }
+ # Check for database lock
+ if( wfReadOnly() ) {
+ $wgOut->readOnlyPage();
+ return;
+ }
+ # Show unblock form
+ $ipu->showForm( '' );
+ } elseif( $action == 'submit' && $wgRequest->wasPosted()
+ && $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) {
+ # Check permissions
+ if( !$wgUser->isAllowed( 'block' ) ) {
+ $wgOut->permissionRequired( 'block' );
+ return;
+ }
+ # Check for database lock
+ if( wfReadOnly() ) {
+ $wgOut->readOnlyPage();
+ return;
+ }
+ # Remove blocks and redirect user to success page
+ $ipu->doSubmit();
+ } elseif( $action == 'success' ) {
+ # Inform the user of a successful unblock
+ # (No need to check permissions or locks here,
+ # if something was done, then it's too late!)
+ if ( substr( $successip, 0, 1) == '#' ) {
+ // A block ID was unblocked
+ $ipu->showList( $wgOut->parse( wfMsg( 'unblocked-id', $successip ) ) );
+ } else {
+ // A username/IP was unblocked
+ $ipu->showList( $wgOut->parse( wfMsg( 'unblocked', $successip ) ) );
+ }
+ } else {
+ # Just show the block list
+ $ipu->showList( '' );
+ }
+
+}
+
+/**
+ * implements Special:ipblocklist GUI
+ * @ingroup SpecialPage
+ */
+class IPUnblockForm {
+ var $ip, $reason, $id;
+
+ function IPUnblockForm( $ip, $id, $reason ) {
+ $this->ip = strtr( $ip, '_', ' ' );
+ $this->id = $id;
+ $this->reason = $reason;
+ }
+
+ /**
+ * Generates the unblock form
+ * @param $err string: error message
+ * @return $out string: HTML form
+ */
+ function showForm( $err ) {
+ global $wgOut, $wgUser, $wgSysopUserBans;
+
+ $wgOut->setPagetitle( wfMsg( 'unblockip' ) );
+ $wgOut->addWikiMsg( 'unblockiptext' );
+
+ $titleObj = SpecialPage::getTitleFor( "Ipblocklist" );
+ $action = $titleObj->getLocalURL( "action=submit" );
+
+ if ( "" != $err ) {
+ $wgOut->setSubtitle( wfMsg( "formerror" ) );
+ $wgOut->addWikiText( Xml::tags( 'span', array( 'class' => 'error' ), $err ) . "\n" );
+ }
+
+ $addressPart = false;
+ if ( $this->id ) {
+ $block = Block::newFromID( $this->id );
+ if ( $block ) {
+ $encName = htmlspecialchars( $block->getRedactedName() );
+ $encId = $this->id;
+ $addressPart = $encName . Xml::hidden( 'id', $encId );
+ $ipa = wfMsgHtml( $wgSysopUserBans ? 'ipadressorusername' : 'ipaddress' );
+ }
+ }
+ if ( !$addressPart ) {
+ $addressPart = Xml::input( 'wpUnblockAddress', 40, $this->ip, array( 'type' => 'text', 'tabindex' => '1' ) );
+ $ipa = Xml::label( wfMsg( $wgSysopUserBans ? 'ipadressorusername' : 'ipaddress' ), 'wpUnblockAddress' );
+ }
+
+ $wgOut->addHTML(
+ Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'unblockip' ) ) .
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', null, wfMsg( 'ipb-unblock' ) ) .
+ Xml::openElement( 'table', array( 'id' => 'mw-unblock-table' ) ).
+ "<tr>
+ <td class='mw-label'>
+ {$ipa}
+ </td>
+ <td class='mw-input'>
+ {$addressPart}
+ </td>
+ </tr>
+ <tr>
+ <td class='mw-label'>" .
+ Xml::label( wfMsg( 'ipbreason' ), 'wpUnblockReason' ) .
+ "</td>
+ <td class='mw-input'>" .
+ Xml::input( 'wpUnblockReason', 40, $this->reason, array( 'type' => 'text', 'tabindex' => '2' ) ) .
+ "</td>
+ </tr>
+ <tr>
+ <td>&nbsp;</td>
+ <td class='mw-submit'>" .
+ Xml::submitButton( wfMsg( 'ipusubmit' ), array( 'name' => 'wpBlock', 'tabindex' => '3' ) ) .
+ "</td>
+ </tr>" .
+ Xml::closeElement( 'table' ) .
+ Xml::closeElement( 'fieldset' ) .
+ Xml::hidden( 'wpEditToken', $wgUser->editToken() ) .
+ Xml::closeElement( 'form' ) . "\n"
+ );
+
+ }
+
+ const UNBLOCK_SUCCESS = 0; // Success
+ const UNBLOCK_NO_SUCH_ID = 1; // No such block ID
+ const UNBLOCK_USER_NOT_BLOCKED = 2; // IP wasn't blocked
+ const UNBLOCK_BLOCKED_AS_RANGE = 3; // IP is part of a range block
+ const UNBLOCK_UNKNOWNERR = 4; // Unknown error
+
+ /**
+ * Backend code for unblocking. doSubmit() wraps around this.
+ * $range is only used when UNBLOCK_BLOCKED_AS_RANGE is returned, in which
+ * case it contains the range $ip is part of.
+ * @return array array(message key, parameters) on failure, empty array on success
+ */
+
+ static function doUnblock(&$id, &$ip, &$reason, &$range = null)
+ {
+ if ( $id ) {
+ $block = Block::newFromID( $id );
+ if ( !$block ) {
+ return array('ipb_cant_unblock', htmlspecialchars($id));
+ }
+ $ip = $block->getRedactedName();
+ } else {
+ $block = new Block();
+ $ip = trim( $ip );
+ if ( substr( $ip, 0, 1 ) == "#" ) {
+ $id = substr( $ip, 1 );
+ $block = Block::newFromID( $id );
+ if( !$block ) {
+ return array('ipb_cant_unblock', htmlspecialchars($id));
+ }
+ $ip = $block->getRedactedName();
+ } else {
+ $block = Block::newFromDB( $ip );
+ if ( !$block ) {
+ return array('ipb_cant_unblock', htmlspecialchars($id));
+ }
+ if( $block->mRangeStart != $block->mRangeEnd
+ && !strstr( $ip, "/" ) ) {
+ /* If the specified IP is a single address, and the block is
+ * a range block, don't unblock the range. */
+ $range = $block->mAddress;
+ return array('ipb_blocked_as_range', $ip, $range);
+ }
+ }
+ }
+ // Yes, this is really necessary
+ $id = $block->mId;
+
+ # Delete block
+ if ( !$block->delete() ) {
+ return array('ipb_cant_unblock', htmlspecialchars($id));
+ }
+
+ # Make log entry
+ $log = new LogPage( 'block' );
+ $log->addEntry( 'unblock', Title::makeTitle( NS_USER, $ip ), $reason );
+ return array();
+ }
+
+ function doSubmit() {
+ global $wgOut;
+ $retval = self::doUnblock($this->id, $this->ip, $this->reason, $range);
+ if(!empty($retval))
+ {
+ $key = array_shift($retval);
+ $this->showForm(wfMsgReal($key, $retval));
+ return;
+ }
+ # Report to the user
+ $titleObj = SpecialPage::getTitleFor( "Ipblocklist" );
+ $success = $titleObj->getFullURL( "action=success&successip=" . urlencode( $this->ip ) );
+ $wgOut->redirect( $success );
+ }
+
+ function showList( $msg ) {
+ global $wgOut, $wgUser;
+
+ $wgOut->setPagetitle( wfMsg( "ipblocklist" ) );
+ if ( "" != $msg ) {
+ $wgOut->setSubtitle( $msg );
+ }
+
+ // Purge expired entries on one in every 10 queries
+ if ( !mt_rand( 0, 10 ) ) {
+ Block::purgeExpired();
+ }
+
+ $conds = array();
+ $matches = array();
+ // Is user allowed to see all the blocks?
+ if ( !$wgUser->isAllowed( 'suppress' ) )
+ $conds['ipb_deleted'] = 0;
+ if ( $this->ip == '' ) {
+ // No extra conditions
+ } elseif ( substr( $this->ip, 0, 1 ) == '#' ) {
+ $conds['ipb_id'] = substr( $this->ip, 1 );
+ } elseif ( IP::toUnsigned( $this->ip ) !== false ) {
+ $conds['ipb_address'] = $this->ip;
+ $conds['ipb_auto'] = 0;
+ } elseif( preg_match( '/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\\/(\\d{1,2})$/', $this->ip, $matches ) ) {
+ $conds['ipb_address'] = Block::normaliseRange( $this->ip );
+ $conds['ipb_auto'] = 0;
+ } else {
+ $user = User::newFromName( $this->ip );
+ if ( $user && ( $id = $user->getId() ) != 0 ) {
+ $conds['ipb_user'] = $id;
+ } else {
+ // Uh...?
+ $conds['ipb_address'] = $this->ip;
+ $conds['ipb_auto'] = 0;
+ }
+ }
+
+ $pager = new IPBlocklistPager( $this, $conds );
+ if ( $pager->getNumRows() ) {
+ $wgOut->addHTML(
+ $this->searchForm() .
+ $pager->getNavigationBar() .
+ Xml::tags( 'ul', null, $pager->getBody() ) .
+ $pager->getNavigationBar()
+ );
+ } elseif ( $this->ip != '') {
+ $wgOut->addHTML( $this->searchForm() );
+ $wgOut->addWikiMsg( 'ipblocklist-no-results' );
+ } else {
+ $wgOut->addWikiMsg( 'ipblocklist-empty' );
+ }
+ }
+
+ function searchForm() {
+ global $wgTitle, $wgScript, $wgRequest;
+ return
+ Xml::tags( 'form', array( 'action' => $wgScript ),
+ Xml::hidden( 'title', $wgTitle->getPrefixedDbKey() ) .
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', null, wfMsg( 'ipblocklist-legend' ) ) .
+ Xml::inputLabel( wfMsg( 'ipblocklist-username' ), 'ip', 'ip', /* size */ false, $this->ip ) .
+ '&nbsp;' .
+ Xml::submitButton( wfMsg( 'ipblocklist-submit' ) ) .
+ Xml::closeElement( 'fieldset' )
+ );
+ }
+
+ /**
+ * Callback function to output a block
+ */
+ function formatRow( $block ) {
+ global $wgUser, $wgLang;
+
+ wfProfileIn( __METHOD__ );
+
+ static $sk=null, $msg=null;
+
+ if( is_null( $sk ) )
+ $sk = $wgUser->getSkin();
+ if( is_null( $msg ) ) {
+ $msg = array();
+ $keys = array( 'infiniteblock', 'expiringblock', 'unblocklink',
+ 'anononlyblock', 'createaccountblock', 'noautoblockblock', 'emailblock' );
+ foreach( $keys as $key ) {
+ $msg[$key] = wfMsgHtml( $key );
+ }
+ $msg['blocklistline'] = wfMsg( 'blocklistline' );
+ }
+
+ # Prepare links to the blocker's user and talk pages
+ $blocker_id = $block->getBy();
+ $blocker_name = $block->getByName();
+ $blocker = $sk->userLink( $blocker_id, $blocker_name );
+ $blocker .= $sk->userToolLinks( $blocker_id, $blocker_name );
+
+ # Prepare links to the block target's user and contribs. pages (as applicable, don't do it for autoblocks)
+ if( $block->mAuto ) {
+ $target = $block->getRedactedName(); # Hide the IP addresses of auto-blocks; privacy
+ } else {
+ $target = $sk->userLink( $block->mUser, $block->mAddress )
+ . $sk->userToolLinks( $block->mUser, $block->mAddress, false, Linker::TOOL_LINKS_NOBLOCK );
+ }
+
+ $formattedTime = $wgLang->timeanddate( $block->mTimestamp, true );
+
+ $properties = array();
+ $properties[] = Block::formatExpiry( $block->mExpiry );
+ if ( $block->mAnonOnly ) {
+ $properties[] = $msg['anononlyblock'];
+ }
+ if ( $block->mCreateAccount ) {
+ $properties[] = $msg['createaccountblock'];
+ }
+ if (!$block->mEnableAutoblock && $block->mUser ) {
+ $properties[] = $msg['noautoblockblock'];
+ }
+
+ if ( $block->mBlockEmail && $block->mUser ) {
+ $properties[] = $msg['emailblock'];
+ }
+
+ $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 ) ) . ')';
+ }
+
+ $comment = $sk->commentBlock( $block->mReason );
+
+ $s = "{$line} $comment";
+ if ( $block->mHideName )
+ $s = '<span class="history-deleted">' . $s . '</span>';
+
+ wfProfileOut( __METHOD__ );
+ return "<li>$s $unblocklink</li>\n";
+ }
+}
+
+/**
+ * @todo document
+ * @ingroup Pager
+ */
+class IPBlocklistPager extends ReverseChronologicalPager {
+ public $mForm, $mConds;
+
+ function __construct( $form, $conds = array() ) {
+ $this->mForm = $form;
+ $this->mConds = $conds;
+ parent::__construct();
+ }
+
+ function getStartBody() {
+ wfProfileIn( __METHOD__ );
+ # Do a link batch query
+ $this->mResult->seek( 0 );
+ $lb = new LinkBatch;
+
+ /*
+ while ( $row = $this->mResult->fetchObject() ) {
+ $lb->addObj( Title::makeTitleSafe( NS_USER, $row->user_name ) );
+ $lb->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->user_name ) );
+ $lb->addObj( Title::makeTitleSafe( NS_USER, $row->ipb_address ) );
+ $lb->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->ipb_address ) );
+ }*/
+ # Faster way
+ # Usernames and titles are in fact related by a simple substitution of space -> underscore
+ # The last few lines of Title::secureAndSplit() tell the story.
+ while ( $row = $this->mResult->fetchObject() ) {
+ $name = str_replace( ' ', '_', $row->ipb_by_text );
+ $lb->add( NS_USER, $name );
+ $lb->add( NS_USER_TALK, $name );
+ $name = str_replace( ' ', '_', $row->ipb_address );
+ $lb->add( NS_USER, $name );
+ $lb->add( NS_USER_TALK, $name );
+ }
+ $lb->execute();
+ wfProfileOut( __METHOD__ );
+ return '';
+ }
+
+ function formatRow( $row ) {
+ $block = new Block;
+ $block->initFromRow( $row );
+ return $this->mForm->formatRow( $block );
+ }
+
+ function getQueryInfo() {
+ $conds = $this->mConds;
+ $conds[] = 'ipb_expiry>' . $this->mDb->addQuotes( $this->mDb->timestamp() );
+ return array(
+ 'tables' => 'ipblocks',
+ 'fields' => '*',
+ 'conds' => $conds,
+ );
+ }
+
+ function getIndexField() {
+ return 'ipb_timestamp';
+ }
+}
diff --git a/includes/specials/SpecialListgrouprights.php b/includes/specials/SpecialListgrouprights.php
new file mode 100644
index 00000000..131c0606
--- /dev/null
+++ b/includes/specials/SpecialListgrouprights.php
@@ -0,0 +1,112 @@
+<?php
+
+/**
+ * This special page lists all defined user groups and the associated rights.
+ * See also @ref $wgGroupPermissions.
+ *
+ * @ingroup SpecialPage
+ * @author Petr Kadlec <mormegil@centrum.cz>
+ */
+class SpecialListGroupRights extends SpecialPage {
+
+ var $skin;
+
+ /**
+ * Constructor
+ */
+ function __construct() {
+ global $wgUser;
+ parent::__construct( 'Listgrouprights' );
+ $this->skin = $wgUser->getSkin();
+ }
+
+ /**
+ * Show the special page
+ */
+ public function execute( $par ) {
+ global $wgOut, $wgGroupPermissions, $wgImplicitGroups, $wgMessageCache;
+ $wgMessageCache->loadAllMessages();
+
+ $this->setHeaders();
+ $this->outputHeader();
+
+ $wgOut->addHTML(
+ Xml::openElement( 'table', array( 'class' => 'mw-listgrouprights-table' ) ) .
+ '<tr>' .
+ Xml::element( 'th', null, wfMsg( 'listgrouprights-group' ) ) .
+ Xml::element( 'th', null, wfMsg( 'listgrouprights-rights' ) ) .
+ '</tr>'
+ );
+
+ foreach( $wgGroupPermissions as $group => $permissions ) {
+ $groupname = ( $group == '*' ) ? 'all' : htmlspecialchars( $group ); // Replace * with a more descriptive groupname
+
+ $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;
+ }
+
+ if( $group == '*' ) {
+ // Do not make a link for the generic * group
+ $grouppage = $groupnameLocalized;
+ } else {
+ $grouppage = $this->skin->makeLink( $grouppageLocalized, $groupnameLocalized );
+ }
+
+ if ( !in_array( $group, $wgImplicitGroups ) ) {
+ $grouplink = '<br />' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Listusers' ), wfMsgHtml( 'listgrouprights-members' ), 'group=' . $group );
+ } else {
+ // No link to Special:listusers for implicit groups as they are unlistable
+ $grouplink = '';
+ }
+
+ $wgOut->addHTML(
+ '<tr>
+ <td>' .
+ $grouppage . $grouplink .
+ '</td>
+ <td>' .
+ self::formatPermissions( $permissions ) .
+ '</td>
+ </tr>'
+ );
+ }
+ $wgOut->addHTML(
+ Xml::closeElement( 'table' ) . "\n"
+ );
+ }
+
+ /**
+ * Create a user-readable list of permissions from the given array.
+ *
+ * @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 ) {
+ $r = array();
+ foreach( $permissions as $permission => $granted ) {
+ if ( $granted ) {
+ $description = wfMsgHTML( 'listgrouprights-right-display',
+ User::getRightDescription($permission),
+ $permission
+ );
+ $r[] = $description;
+ }
+ }
+ sort( $r );
+ if( empty( $r ) ) {
+ return '';
+ } else {
+ return '<ul><li>' . implode( "</li>\n<li>", $r ) . '</li></ul>';
+ }
+ }
+}
diff --git a/includes/specials/SpecialListredirects.php b/includes/specials/SpecialListredirects.php
new file mode 100644
index 00000000..808aab14
--- /dev/null
+++ b/includes/specials/SpecialListredirects.php
@@ -0,0 +1,58 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ *
+ * @author Rob Church <robchur@gmail.com>
+ * @copyright © 2006 Rob Church
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ */
+
+/**
+ * Special:Listredirects - Lists all the redirects on the wiki.
+ * @ingroup SpecialPage
+ */
+class ListredirectsPage extends QueryPage {
+
+ function getName() { return( 'Listredirects' ); }
+ function isExpensive() { return( true ); }
+ function isSyndicated() { return( false ); }
+ function sortDescending() { return( false ); }
+
+ 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";
+ return( $sql );
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgContLang;
+
+ # Make a link to the redirect itself
+ $rd_title = Title::makeTitle( $result->namespace, $result->title );
+ $rd_link = $skin->makeLinkObj( $rd_title, '', 'redirect=no' );
+
+ # Find out where the redirect leads
+ $revision = Revision::newFromTitle( $rd_title );
+ if( $revision ) {
+ # Make a link to the destination page
+ $target = Title::newFromRedirect( $revision->getText() );
+ if( $target ) {
+ $arr = $wgContLang->getArrow() . $wgContLang->getDirMark();
+ $targetLink = $skin->makeLinkObj( $target );
+ return "$rd_link $arr $targetLink";
+ } else {
+ return "<s>$rd_link</s>";
+ }
+ } else {
+ return "<s>$rd_link</s>";
+ }
+ }
+}
+
+function wfSpecialListredirects() {
+ list( $limit, $offset ) = wfCheckLimits();
+ $lrp = new ListredirectsPage();
+ $lrp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialListusers.php b/includes/specials/SpecialListusers.php
new file mode 100644
index 00000000..7dba44e2
--- /dev/null
+++ b/includes/specials/SpecialListusers.php
@@ -0,0 +1,235 @@
+<?php
+
+# Copyright (C) 2004 Brion Vibber, lcrocker, Tim Starling,
+# Domas Mituzas, Ashar Voultoiz, Jens Frank, Zhengzhu.
+#
+# © 2006 Rob Church <robchur@gmail.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
+ */
+
+/**
+ * This class is used to get a list of user. The ones with specials
+ * rights (sysop, bureaucrat, developer) will have them displayed
+ * next to their names.
+ *
+ * @ingroup SpecialPage
+ */
+class UsersPager extends AlphabeticPager {
+
+ function __construct($group=null) {
+ global $wgRequest;
+ $this->requestedGroup = $group != "" ? $group : $wgRequest->getVal( 'group' );
+ $un = $wgRequest->getText( 'username' );
+ $this->requestedUser = '';
+ if ( $un != '' ) {
+ $username = Title::makeTitleSafe( NS_USER, $un );
+ if( ! is_null( $username ) ) {
+ $this->requestedUser = $username->getText();
+ }
+ }
+ parent::__construct();
+ }
+
+
+ function getIndexField() {
+ return 'user_name';
+ }
+
+ function getQueryInfo() {
+ $dbr = wfGetDB( DB_SLAVE );
+ $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 = '';
+ } else {
+ $useIndex = $dbr->useIndexClause('user_name');
+ }
+ if ($this->requestedUser != "") {
+ $conds[] = 'user_name >= ' . $dbr->addQuotes( $this->requestedUser );
+ }
+
+ list ($user,$user_groups,$ipblocks) = $dbr->tableNamesN('user','user_groups','ipblocks');
+
+ $query = array(
+ 'tables' => " $user $useIndex LEFT JOIN $user_groups ON user_id=ug_user
+ LEFT JOIN $ipblocks ON user_id=ipb_user AND ipb_auto=0 ",
+ 'fields' => array('user_name',
+ 'MAX(user_id) AS user_id',
+ 'COUNT(ug_group) AS numgroups',
+ 'MAX(ug_group) AS singlegroup'),
+ 'options' => array('GROUP BY' => 'user_name'),
+ 'conds' => $conds
+ );
+
+ wfRunHooks( 'SpecialListusersQueryInfo', array( $this, &$query ) );
+ return $query;
+ }
+
+ function formatRow( $row ) {
+ $userPage = Title::makeTitle( NS_USER, $row->user_name );
+ $name = $this->getSkin()->makeLinkObj( $userPage, htmlspecialchars( $userPage->getText() ) );
+
+ if( $row->numgroups > 1 || ( $this->requestedGroup && $row->numgroups == 1 ) ) {
+ $list = array();
+ foreach( self::getGroups( $row->user_id ) as $group )
+ $list[] = self::buildGroupLink( $group );
+ $groups = implode( ', ', $list );
+ } elseif( $row->numgroups == 1 ) {
+ $groups = self::buildGroupLink( $row->singlegroup );
+ } else {
+ $groups = '';
+ }
+
+ $item = wfSpecialList( $name, $groups );
+ wfRunHooks( 'SpecialListusersFormatRow', array( &$item, $row ) );
+ return "<li>{$item}</li>";
+ }
+
+ function getBody() {
+ if (!$this->mQueryDone) {
+ $this->doQuery();
+ }
+ $batch = new LinkBatch;
+
+ $this->mResult->rewind();
+
+ while ( $row = $this->mResult->fetchObject() ) {
+ $batch->addObj( Title::makeTitleSafe( NS_USER, $row->user_name ) );
+ }
+ $batch->execute();
+ $this->mResult->rewind();
+ return parent::getBody();
+ }
+
+ function getPageHeader( ) {
+ global $wgScript, $wgRequest;
+ $self = $this->getTitle();
+
+ # Form tag
+ $out = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) .
+ '<fieldset>' .
+ Xml::element( 'legend', array(), wfMsg( 'listusers' ) );
+ $out .= Xml::hidden( 'title', $self->getPrefixedDbKey() );
+
+ # Username field
+ $out .= Xml::label( wfMsg( 'listusersfrom' ), 'offset' ) . ' ' .
+ Xml::input( 'username', 20, $this->requestedUser, array( 'id' => 'offset' ) ) . ' ';
+
+ # Group drop-down list
+ $out .= Xml::label( wfMsg( 'group' ), 'group' ) . ' ' .
+ Xml::openElement('select', array( 'name' => 'group', 'id' => 'group' ) ) .
+ Xml::option( wfMsg( 'group-all' ), '' );
+ foreach( $this->getAllGroups() as $group => $groupText )
+ $out .= Xml::option( $groupText, $group, $group == $this->requestedGroup );
+ $out .= Xml::closeElement( 'select' ) . ' ';
+
+ wfRunHooks( 'SpecialListusersHeaderForm', array( $this, &$out ) );
+
+ # Submit button and form bottom
+ if( $this->mLimit )
+ $out .= Xml::hidden( 'limit', $this->mLimit );
+ $out .= Xml::submitButton( wfMsg( 'allpagessubmit' ) );
+ wfRunHooks( 'SpecialListusersHeader', array( $this, &$out ) );
+ $out .= '</fieldset>' .
+ Xml::closeElement( 'form' );
+
+ return $out;
+ }
+
+ function getAllGroups() {
+ $result = array();
+ foreach( User::getAllGroups() as $group ) {
+ $result[$group] = User::getGroupName( $group );
+ }
+ return $result;
+ }
+
+ /**
+ * Preserve group and username offset parameters when paging
+ * @return array
+ */
+ function getDefaultQuery() {
+ $query = parent::getDefaultQuery();
+ if( $this->requestedGroup != '' )
+ $query['group'] = $this->requestedGroup;
+ if( $this->requestedUser != '' )
+ $query['username'] = $this->requestedUser;
+ wfRunHooks( 'SpecialListusersDefaultQuery', array( $this, &$query ) );
+ return $query;
+ }
+
+ /**
+ * Get a list of groups the specified user belongs to
+ *
+ * @param int $uid
+ * @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 );
+ }
+ return $groups;
+ }
+
+ /**
+ * Format a link to a group description page
+ *
+ * @param string $group
+ * @return string
+ */
+ protected static function buildGroupLink( $group ) {
+ static $cache = array();
+ if( !isset( $cache[$group] ) )
+ $cache[$group] = User::makeGroupLinkHtml( $group, User::getGroupMember( $group ) );
+ return $cache[$group];
+ }
+}
+
+/**
+ * constructor
+ * $par string (optional) A group to list users from
+ */
+function wfSpecialListusers( $par = null ) {
+ global $wgRequest, $wgOut;
+
+ $up = new UsersPager($par);
+
+ # getBody() first to check, if empty
+ $usersbody = $up->getBody();
+ $s = $up->getPageHeader();
+ if( $usersbody ) {
+ $s .= $up->getNavigationBar();
+ $s .= '<ul>' . $usersbody . '</ul>';
+ $s .= $up->getNavigationBar() ;
+ } else {
+ $s .= '<p>' . wfMsgHTML('listusers-noresult') . '</p>';
+ };
+
+ $wgOut->addHTML( $s );
+}
diff --git a/includes/specials/SpecialLockdb.php b/includes/specials/SpecialLockdb.php
new file mode 100644
index 00000000..04019223
--- /dev/null
+++ b/includes/specials/SpecialLockdb.php
@@ -0,0 +1,131 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Constructor
+ */
+function wfSpecialLockdb() {
+ global $wgUser, $wgOut, $wgRequest;
+
+ if( !$wgUser->isAllowed( 'siteadmin' ) ) {
+ $wgOut->permissionRequired( 'siteadmin' );
+ return;
+ }
+
+ # If the lock file isn't writable, we can do sweet bugger all
+ global $wgReadOnlyFile;
+ if( !is_writable( dirname( $wgReadOnlyFile ) ) ) {
+ DBLockForm::notWritable();
+ return;
+ }
+
+ $action = $wgRequest->getVal( 'action' );
+ $f = new DBLockForm();
+
+ if ( 'success' == $action ) {
+ $f->showSuccess();
+ } else if ( 'submit' == $action && $wgRequest->wasPosted() &&
+ $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) {
+ $f->doSubmit();
+ } else {
+ $f->showForm( '' );
+ }
+}
+
+/**
+ * A form to make the database readonly (eg for maintenance purposes).
+ * @ingroup SpecialPage
+ */
+class DBLockForm {
+ var $reason = '';
+
+ function DBLockForm() {
+ global $wgRequest;
+ $this->reason = $wgRequest->getText( 'wpLockReason' );
+ }
+
+ function showForm( $err ) {
+ global $wgOut, $wgUser;
+
+ $wgOut->setPagetitle( wfMsg( 'lockdb' ) );
+ $wgOut->addWikiMsg( 'lockdbtext' );
+
+ if ( "" != $err ) {
+ $wgOut->setSubtitle( wfMsg( 'formerror' ) );
+ $wgOut->addHTML( '<p class="error">' . htmlspecialchars( $err ) . "</p>\n" );
+ }
+ $lc = htmlspecialchars( wfMsg( 'lockconfirm' ) );
+ $lb = htmlspecialchars( wfMsg( 'lockbtn' ) );
+ $elr = htmlspecialchars( wfMsg( 'enterlockreason' ) );
+ $titleObj = SpecialPage::getTitleFor( 'Lockdb' );
+ $action = $titleObj->escapeLocalURL( 'action=submit' );
+ $reason = htmlspecialchars( $this->reason );
+ $token = htmlspecialchars( $wgUser->editToken() );
+
+ $wgOut->addHTML( <<<END
+<form id="lockdb" method="post" action="{$action}">
+{$elr}:
+<textarea name="wpLockReason" rows="10" cols="60" wrap="virtual">{$reason}</textarea>
+<table border="0">
+ <tr>
+ <td align="right">
+ <input type="checkbox" name="wpLockConfirm" />
+ </td>
+ <td align="left">{$lc}</td>
+ </tr>
+ <tr>
+ <td>&nbsp;</td>
+ <td align="left">
+ <input type="submit" name="wpLock" value="{$lb}" />
+ </td>
+ </tr>
+</table>
+<input type="hidden" name="wpEditToken" value="{$token}" />
+</form>
+END
+);
+
+ }
+
+ function doSubmit() {
+ global $wgOut, $wgUser, $wgLang, $wgRequest;
+ global $wgReadOnlyFile;
+
+ if ( ! $wgRequest->getCheck( 'wpLockConfirm' ) ) {
+ $this->showForm( wfMsg( 'locknoconfirm' ) );
+ return;
+ }
+ $fp = @fopen( $wgReadOnlyFile, 'w' );
+
+ if ( false === $fp ) {
+ # This used to show a file not found error, but the likeliest reason for fopen()
+ # to fail at this point is insufficient permission to write to the file...good old
+ # is_writable() is plain wrong in some cases, it seems...
+ self::notWritable();
+ return;
+ }
+ fwrite( $fp, $this->reason );
+ fwrite( $fp, "\n<p>(by " . $wgUser->getName() . " at " .
+ $wgLang->timeanddate( wfTimestampNow() ) . ")\n" );
+ fclose( $fp );
+
+ $titleObj = SpecialPage::getTitleFor( 'Lockdb' );
+ $wgOut->redirect( $titleObj->getFullURL( 'action=success' ) );
+ }
+
+ function showSuccess() {
+ global $wgOut;
+
+ $wgOut->setPagetitle( wfMsg( 'lockdb' ) );
+ $wgOut->setSubtitle( wfMsg( 'lockdbsuccesssub' ) );
+ $wgOut->addWikiMsg( 'lockdbsuccesstext' );
+ }
+
+ public static function notWritable() {
+ global $wgOut;
+ $wgOut->showErrorPage( 'lockdb', 'lockfilenotwritable' );
+ }
+}
diff --git a/includes/specials/SpecialLog.php b/includes/specials/SpecialLog.php
new file mode 100644
index 00000000..3154ed13
--- /dev/null
+++ b/includes/specials/SpecialLog.php
@@ -0,0 +1,65 @@
+<?php
+# Copyright (C) 2008 Aaron Schulz
+# 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
+ */
+
+/**
+ * constructor
+ */
+function wfSpecialLog( $par = '' ) {
+ global $wgRequest, $wgOut, $wgUser;
+ # Get parameters
+ $type = $wgRequest->getVal( 'type', $par );
+ $user = $wgRequest->getText( 'user' );
+ $title = $wgRequest->getText( 'page' );
+ $pattern = $wgRequest->getBool( 'pattern' );
+ $y = $wgRequest->getIntOrNull( 'year' );
+ $m = $wgRequest->getIntOrNull( 'month' );
+ # Don't let the user get stuck with a certain date
+ $skip = $wgRequest->getText( 'offset' ) || $wgRequest->getText( 'dir' ) == 'prev';
+ if( $skip ) {
+ $y = '';
+ $m = '';
+ }
+ # 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() );
+ # Insert list
+ $logBody = $pager->getBody();
+ if( $logBody ) {
+ $wgOut->addHTML(
+ $pager->getNavigationBar() .
+ $loglist->beginLogEventsList() .
+ $logBody .
+ $loglist->endLogEventsList() .
+ $pager->getNavigationBar()
+ );
+ } else {
+ $wgOut->addWikiMsg( 'logempty' );
+ }
+}
diff --git a/includes/specials/SpecialLonelypages.php b/includes/specials/SpecialLonelypages.php
new file mode 100644
index 00000000..5aafac7d
--- /dev/null
+++ b/includes/specials/SpecialLonelypages.php
@@ -0,0 +1,58 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * A special page looking for articles with no article linking to them,
+ * thus being lonely.
+ * @ingroup SpecialPage
+ */
+class LonelyPagesPage extends PageQueryPage {
+
+ function getName() {
+ return "Lonelypages";
+ }
+ function getPageHeader() {
+ return wfMsgExt( 'lonelypagestext', array( 'parse' ) );
+ }
+
+ function sortDescending() {
+ return false;
+ }
+
+ function isExpensive() {
+ return true;
+ }
+ function isSyndicated() { return false; }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ list( $page, $pagelinks ) = $dbr->tableNamesN( 'page', 'pagelinks' );
+
+ return
+ "SELECT 'Lonelypages' AS type,
+ page_namespace AS namespace,
+ page_title AS title,
+ page_title AS value
+ FROM $page
+ LEFT JOIN $pagelinks
+ ON page_namespace=pl_namespace AND page_title=pl_title
+ WHERE pl_namespace IS NULL
+ AND page_namespace=".NS_MAIN."
+ AND page_is_redirect=0";
+
+ }
+}
+
+/**
+ * Constructor
+ */
+function wfSpecialLonelypages() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $lpp = new LonelyPagesPage();
+
+ return $lpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialLongpages.php b/includes/specials/SpecialLongpages.php
new file mode 100644
index 00000000..be16a029
--- /dev/null
+++ b/includes/specials/SpecialLongpages.php
@@ -0,0 +1,31 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ *
+ * @ingroup SpecialPage
+ */
+class LongPagesPage extends ShortPagesPage {
+
+ function getName() {
+ return "Longpages";
+ }
+
+ function sortDescending() {
+ return true;
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialLongpages() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $lpp = new LongPagesPage();
+
+ $lpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialMIMEsearch.php b/includes/specials/SpecialMIMEsearch.php
new file mode 100644
index 00000000..82ee4be6
--- /dev/null
+++ b/includes/specials/SpecialMIMEsearch.php
@@ -0,0 +1,138 @@
+<?php
+/**
+ * A special page to search for files by MIME type as defined in the
+ * img_major_mime and img_minor_mime fields in the image table
+ *
+ * @file
+ * @ingroup SpecialPage
+ *
+ * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ */
+
+/**
+ * Searches the database for files of the requested MIME type, comparing this with the
+ * 'img_major_mime' and 'img_minor_mime' fields in the image table.
+ * @ingroup SpecialPage
+ */
+class MIMEsearchPage extends QueryPage {
+ var $major, $minor;
+
+ function MIMEsearchPage( $major, $minor ) {
+ $this->major = $major;
+ $this->minor = $minor;
+ }
+
+ function getName() { return 'MIMEsearch'; }
+
+ /**
+ * Due to this page relying upon extra fields being passed in the SELECT it
+ * will fail if it's set as expensive and misermode is on
+ */
+ function isExpensive() { return true; }
+ function isSyndicated() { return false; }
+
+ function linkParameters() {
+ $arr = array( $this->major, $this->minor );
+ $mime = implode( '/', $arr );
+ return array( 'mime' => $mime );
+ }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ $image = $dbr->tableName( 'image' );
+ $major = $dbr->addQuotes( $this->major );
+ $minor = $dbr->addQuotes( $this->minor );
+
+ return
+ "SELECT 'MIMEsearch' AS type,
+ " . NS_IMAGE . " AS namespace,
+ img_name AS title,
+ img_major_mime AS value,
+
+ img_size,
+ img_width,
+ img_height,
+ img_user_text,
+ img_timestamp
+ FROM $image
+ WHERE img_major_mime = $major AND img_minor_mime = $minor
+ ";
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgContLang, $wgLang;
+
+ $nt = Title::makeTitle( $result->namespace, $result->title );
+ $text = $wgContLang->convert( $nt->getText() );
+ $plink = $skin->makeLink( $nt->getPrefixedText(), $text );
+
+ $download = $skin->makeMediaLinkObj( $nt, wfMsgHtml( 'download' ) );
+ $bytes = wfMsgExt( 'nbytes', array( 'parsemag', 'escape'),
+ $wgLang->formatNum( $result->img_size ) );
+ $dimensions = wfMsgHtml( 'widthheight', $wgLang->formatNum( $result->img_width ),
+ $wgLang->formatNum( $result->img_height ) );
+ $user = $skin->makeLinkObj( Title::makeTitle( NS_USER, $result->img_user_text ), $result->img_user_text );
+ $time = $wgLang->timeanddate( $result->img_timestamp );
+
+ return "($download) $plink . . $dimensions . . $bytes . . $user . . $time";
+ }
+}
+
+/**
+ * Output the HTML search form, and constructs the MIMEsearchPage object.
+ */
+function wfSpecialMIMEsearch( $par = null ) {
+ global $wgRequest, $wgTitle, $wgOut;
+
+ $mime = isset( $par ) ? $par : $wgRequest->getText( 'mime' );
+
+ $wgOut->addHTML(
+ Xml::openElement( 'form', array( 'id' => 'specialmimesearch', 'method' => 'get', 'action' => $wgTitle->getLocalUrl() ) ) .
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', null, wfMsg( 'mimesearch' ) ) .
+ Xml::inputLabel( wfMsg( 'mimetype' ), 'mime', 'mime', 20, $mime ) . ' ' .
+ Xml::submitButton( wfMsg( 'ilsubmit' ) ) .
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' )
+ );
+
+ list( $major, $minor ) = wfSpecialMIMEsearchParse( $mime );
+ if ( $major == '' or $minor == '' or !wfSpecialMIMEsearchValidType( $major ) )
+ return;
+ $wpp = new MIMEsearchPage( $major, $minor );
+
+ list( $limit, $offset ) = wfCheckLimits();
+ $wpp->doQuery( $offset, $limit );
+}
+
+function wfSpecialMIMEsearchParse( $str ) {
+ // searched for an invalid MIME type.
+ if( strpos( $str, '/' ) === false) {
+ return array ('', '');
+ }
+
+ list( $major, $minor ) = explode( '/', $str, 2 );
+
+ return array(
+ ltrim( $major, ' ' ),
+ rtrim( $minor, ' ' )
+ );
+}
+
+function wfSpecialMIMEsearchValidType( $type ) {
+ // From maintenance/tables.sql => img_major_mime
+ $types = array(
+ 'unknown',
+ 'application',
+ 'audio',
+ 'image',
+ 'text',
+ 'video',
+ 'message',
+ 'model',
+ 'multipart'
+ );
+
+ return in_array( $type, $types );
+}
diff --git a/includes/specials/SpecialMergeHistory.php b/includes/specials/SpecialMergeHistory.php
new file mode 100644
index 00000000..0460c207
--- /dev/null
+++ b/includes/specials/SpecialMergeHistory.php
@@ -0,0 +1,448 @@
+<?php
+/**
+ * Special page allowing users with the appropriate permissions to
+ * merge article histories, with some restrictions
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Constructor
+ */
+function wfSpecialMergehistory( $par ) {
+ global $wgRequest;
+
+ $form = new MergehistoryForm( $wgRequest, $par );
+ $form->execute();
+}
+
+/**
+ * The HTML form for Special:MergeHistory, which allows users with the appropriate
+ * permissions to view and restore deleted content.
+ * @ingroup SpecialPage
+ */
+class MergehistoryForm {
+ var $mAction, $mTarget, $mDest, $mTimestamp, $mTargetID, $mDestID, $mComment;
+ var $mTargetObj, $mDestObj;
+
+ function MergehistoryForm( $request, $par = "" ) {
+ global $wgUser;
+
+ $this->mAction = $request->getVal( 'action' );
+ $this->mTarget = $request->getVal( 'target' );
+ $this->mDest = $request->getVal( 'dest' );
+ $this->mSubmitted = $request->getBool( 'submitted' );
+
+ $this->mTargetID = intval( $request->getVal( 'targetID' ) );
+ $this->mDestID = intval( $request->getVal( 'destID' ) );
+ $this->mTimestamp = $request->getVal( 'mergepoint' );
+ if( !preg_match("/[0-9]{14}/",$this->mTimestamp) ) {
+ $this->mTimestamp = '';
+ }
+ $this->mComment = $request->getText( 'wpComment' );
+
+ $this->mMerge = $request->wasPosted() && $wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) );
+ // target page
+ if( $this->mSubmitted ) {
+ $this->mTargetObj = Title::newFromURL( $this->mTarget );
+ $this->mDestObj = Title::newFromURL( $this->mDest );
+ } else {
+ $this->mTargetObj = null;
+ $this->mDestObj = null;
+ }
+
+ $this->preCacheMessages();
+ }
+
+ /**
+ * As we use the same small set of messages in various methods and that
+ * they are called often, we call them once and save them in $this->message
+ */
+ function preCacheMessages() {
+ // Precache various messages
+ if( !isset( $this->message ) ) {
+ $this->message['last'] = wfMsgExt( 'last', array( 'escape') );
+ }
+ }
+
+ function execute() {
+ global $wgOut, $wgUser;
+
+ $wgOut->setPagetitle( wfMsgHtml( "mergehistory" ) );
+
+ if( $this->mTargetID && $this->mDestID && $this->mAction=="submit" && $this->mMerge ) {
+ return $this->merge();
+ }
+
+ if ( !$this->mSubmitted ) {
+ $this->showMergeForm();
+ return;
+ }
+
+ $errors = array();
+ if ( !$this->mTargetObj instanceof Title ) {
+ $errors[] = wfMsgExt( 'mergehistory-invalid-source', array( 'parse' ) );
+ } elseif( !$this->mTargetObj->exists() ) {
+ $errors[] = wfMsgExt( 'mergehistory-no-source', array( 'parse' ),
+ wfEscapeWikiText( $this->mTargetObj->getPrefixedText() )
+ );
+ }
+
+ if ( !$this->mDestObj instanceof Title) {
+ $errors[] = wfMsgExt( 'mergehistory-invalid-destination', array( 'parse' ) );
+ } elseif( !$this->mDestObj->exists() ) {
+ $errors[] = wfMsgExt( 'mergehistory-no-destination', array( 'parse' ),
+ wfEscapeWikiText( $this->mDestObj->getPrefixedText() )
+ );
+ }
+
+ // TODO: warn about target = dest?
+
+ if ( count( $errors ) ) {
+ $this->showMergeForm();
+ $wgOut->addHTML( implode( "\n", $errors ) );
+ } else {
+ $this->showHistory();
+ }
+
+ }
+
+ function showMergeForm() {
+ global $wgOut, $wgScript;
+
+ $wgOut->addWikiMsg( 'mergehistory-header' );
+
+ $wgOut->addHtml(
+ Xml::openElement( 'form', array(
+ 'method' => 'get',
+ 'action' => $wgScript ) ) .
+ '<fieldset>' .
+ Xml::element( 'legend', array(),
+ wfMsg( 'mergehistory-box' ) ) .
+ Xml::hidden( 'title',
+ SpecialPage::getTitleFor( 'Mergehistory' )->getPrefixedDbKey() ) .
+ Xml::hidden( 'submitted', '1' ) .
+ Xml::hidden( 'mergepoint', $this->mTimestamp ) .
+ Xml::openElement( 'table' ) .
+ "<tr>
+ <td>".Xml::label( wfMsg( 'mergehistory-from' ), 'target' )."</td>
+ <td>".Xml::input( 'target', 30, $this->mTarget, array('id'=>'target') )."</td>
+ </tr><tr>
+ <td>".Xml::label( wfMsg( 'mergehistory-into' ), 'dest' )."</td>
+ <td>".Xml::input( 'dest', 30, $this->mDest, array('id'=>'dest') )."</td>
+ </tr><tr><td>" .
+ Xml::submitButton( wfMsg( 'mergehistory-go' ) ) .
+ "</td></tr>" .
+ Xml::closeElement( 'table' ) .
+ '</fieldset>' .
+ '</form>' );
+ }
+
+ private function showHistory() {
+ global $wgLang, $wgContLang, $wgUser, $wgOut;
+
+ $this->sk = $wgUser->getSkin();
+
+ $wgOut->setPagetitle( wfMsg( "mergehistory" ) );
+
+ $this->showMergeForm();
+
+ # List all stored revisions
+ $revisions = new MergeHistoryPager( $this, array(), $this->mTargetObj, $this->mDestObj );
+ $haveRevisions = $revisions && $revisions->getNumRows() > 0;
+
+ $titleObj = SpecialPage::getTitleFor( "Mergehistory" );
+ $action = $titleObj->getLocalURL( "action=submit" );
+ # Start the form here
+ $top = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'merge' ) );
+ $wgOut->addHtml( $top );
+
+ if( $haveRevisions ) {
+ # Format the user-visible controls (comment field, submission button)
+ # in a nice little table
+ $align = $wgContLang->isRtl() ? 'left' : 'right';
+ $table =
+ Xml::openElement( 'fieldset' ) .
+ Xml::openElement( 'table' ) .
+ "<tr>
+ <td colspan='2'>" .
+ wfMsgExt( 'mergehistory-merge', array('parseinline'),
+ $this->mTargetObj->getPrefixedText(), $this->mDestObj->getPrefixedText() ) .
+ "</td>
+ </tr>
+ <tr>
+ <td align='$align'>" .
+ Xml::label( wfMsg( 'undeletecomment' ), 'wpComment' ) .
+ "</td>
+ <td>" .
+ Xml::input( 'wpComment', 50, $this->mComment ) .
+ "</td>
+ </tr>
+ <tr>
+ <td>&nbsp;</td>
+ <td>" .
+ Xml::submitButton( wfMsg( 'mergehistory-submit' ), array( 'name' => 'merge', 'id' => 'mw-merge-submit' ) ) .
+ "</td>
+ </tr>" .
+ Xml::closeElement( 'table' ) .
+ Xml::closeElement( 'fieldset' );
+
+ $wgOut->addHtml( $table );
+ }
+
+ $wgOut->addHTML( "<h2 id=\"mw-mergehistory\">" . wfMsgHtml( "mergehistory-list" ) . "</h2>\n" );
+
+ if( $haveRevisions ) {
+ $wgOut->addHTML( $revisions->getNavigationBar() );
+ $wgOut->addHTML( "<ul>" );
+ $wgOut->addHTML( $revisions->getBody() );
+ $wgOut->addHTML( "</ul>" );
+ $wgOut->addHTML( $revisions->getNavigationBar() );
+ } else {
+ $wgOut->addWikiMsg( "mergehistory-empty" );
+ }
+
+ # Show relevant lines from the deletion log:
+ $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'merge' ) ) . "</h2>\n" );
+ LogEventsList::showLogExtract( $wgOut, 'merge', $this->mTargetObj->getPrefixedText() );
+
+ # When we submit, go by page ID to avoid some nasty but unlikely collisions.
+ # Such would happen if a page was renamed after the form loaded, but before submit
+ $misc = Xml::hidden( 'targetID', $this->mTargetObj->getArticleID() );
+ $misc .= Xml::hidden( 'destID', $this->mDestObj->getArticleID() );
+ $misc .= Xml::hidden( 'target', $this->mTarget );
+ $misc .= Xml::hidden( 'dest', $this->mDest );
+ $misc .= Xml::hidden( 'wpEditToken', $wgUser->editToken() );
+ $misc .= Xml::closeElement( 'form' );
+ $wgOut->addHtml( $misc );
+
+ return true;
+ }
+
+ function formatRevisionRow( $row ) {
+ global $wgUser, $wgLang;
+
+ $rev = new Revision( $row );
+
+ $stxt = '';
+ $last = $this->message['last'];
+
+ $ts = wfTimestamp( TS_MW, $row->rev_timestamp );
+ $checkBox = wfRadio( "mergepoint", $ts, false );
+
+ $pageLink = $this->sk->makeKnownLinkObj( $rev->getTitle(),
+ htmlspecialchars( $wgLang->timeanddate( $ts ) ), 'oldid=' . $rev->getId() );
+ if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
+ $pageLink = '<span class="history-deleted">' . $pageLink . '</span>';
+ }
+
+ # Last link
+ if( !$rev->userCan( Revision::DELETED_TEXT ) )
+ $last = $this->message['last'];
+ else if( isset($this->prevId[$row->rev_id]) )
+ $last = $this->sk->makeKnownLinkObj( $rev->getTitle(), $this->message['last'],
+ "diff=" . $row->rev_id . "&oldid=" . $this->prevId[$row->rev_id] );
+
+ $userLink = $this->sk->revUserTools( $rev );
+
+ if(!is_null($size = $row->rev_len)) {
+ $stxt = $this->sk->formatRevisionSize( $size );
+ }
+ $comment = $this->sk->revComment( $rev );
+
+ return "<li>$checkBox ($last) $pageLink . . $userLink $stxt $comment</li>";
+ }
+
+ /**
+ * Fetch revision text link if it's available to all users
+ * @return string
+ */
+ function getPageLink( $row, $titleObj, $ts, $target ) {
+ global $wgLang;
+
+ if( !$this->userCan($row, Revision::DELETED_TEXT) ) {
+ return '<span class="history-deleted">' . $wgLang->timeanddate( $ts, true ) . '</span>';
+ } else {
+ $link = $this->sk->makeKnownLinkObj( $titleObj,
+ $wgLang->timeanddate( $ts, true ), "target=$target&timestamp=$ts" );
+ if( $this->isDeleted($row, Revision::DELETED_TEXT) )
+ $link = '<span class="history-deleted">' . $link . '</span>';
+ return $link;
+ }
+ }
+
+ function merge() {
+ global $wgOut, $wgUser;
+ # Get the titles directly from the IDs, in case the target page params
+ # were spoofed. The queries are done based on the IDs, so it's best to
+ # keep it consistent...
+ $targetTitle = Title::newFromID( $this->mTargetID );
+ $destTitle = Title::newFromID( $this->mDestID );
+ if( is_null($targetTitle) || is_null($destTitle) )
+ return false; // validate these
+ if( $targetTitle->getArticleId() == $destTitle->getArticleId() )
+ return false;
+ # Verify that this timestamp is valid
+ # Must be older than the destination page
+ $dbw = wfGetDB( DB_MASTER );
+ # Get timestamp into DB format
+ $this->mTimestamp = $this->mTimestamp ? $dbw->timestamp($this->mTimestamp) : '';
+ # Max timestamp should be min of destination page
+ $maxtimestamp = $dbw->selectField( 'revision', 'MIN(rev_timestamp)',
+ array('rev_page' => $this->mDestID ),
+ __METHOD__ );
+ # Destination page must exist with revisions
+ if( !$maxtimestamp ) {
+ $wgOut->addWikiMsg('mergehistory-fail');
+ return false;
+ }
+ # Get the latest timestamp of the source
+ $lasttimestamp = $dbw->selectField( array('page','revision'),
+ 'rev_timestamp',
+ array('page_id' => $this->mTargetID, 'page_latest = rev_id' ),
+ __METHOD__ );
+ # $this->mTimestamp must be older than $maxtimestamp
+ if( $this->mTimestamp >= $maxtimestamp ) {
+ $wgOut->addWikiMsg('mergehistory-fail');
+ return false;
+ }
+ # Update the revisions
+ if( $this->mTimestamp ) {
+ $timewhere = "rev_timestamp <= {$this->mTimestamp}";
+ $TimestampLimit = wfTimestamp(TS_MW,$this->mTimestamp);
+ } else {
+ $timewhere = "rev_timestamp <= {$maxtimestamp}";
+ $TimestampLimit = wfTimestamp(TS_MW,$lasttimestamp);
+ }
+ # Do the moving...
+ $dbw->update( 'revision',
+ array( 'rev_page' => $this->mDestID ),
+ array( 'rev_page' => $this->mTargetID,
+ $timewhere ),
+ __METHOD__ );
+
+ $count = $dbw->affectedRows();
+ # Make the source page a redirect if no revisions are left
+ $haveRevisions = $dbw->selectField( 'revision',
+ 'rev_timestamp',
+ array( 'rev_page' => $this->mTargetID ),
+ __METHOD__,
+ array( 'FOR UPDATE' ) );
+ if( !$haveRevisions ) {
+ if( $this->mComment ) {
+ $comment = wfMsgForContent( 'mergehistory-comment', $targetTitle->getPrefixedText(),
+ $destTitle->getPrefixedText(), $this->mComment );
+ } else {
+ $comment = wfMsgForContent( 'mergehistory-autocomment', $targetTitle->getPrefixedText(),
+ $destTitle->getPrefixedText() );
+ }
+ $mwRedir = MagicWord::get( 'redirect' );
+ $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $destTitle->getPrefixedText() . "]]\n";
+ $redirectArticle = new Article( $targetTitle );
+ $redirectRevision = new Revision( array(
+ 'page' => $this->mTargetID,
+ 'comment' => $comment,
+ 'text' => $redirectText ) );
+ $redirectRevision->insertOn( $dbw );
+ $redirectArticle->updateRevisionOn( $dbw, $redirectRevision );
+
+ # Now, we record the link from the redirect to the new title.
+ # It should have no other outgoing links...
+ $dbw->delete( 'pagelinks', array( 'pl_from' => $this->mDestID ), __METHOD__ );
+ $dbw->insert( 'pagelinks',
+ array(
+ 'pl_from' => $this->mDestID,
+ 'pl_namespace' => $destTitle->getNamespace(),
+ 'pl_title' => $destTitle->getDBkey() ),
+ __METHOD__ );
+ } else {
+ $targetTitle->invalidateCache(); // update histories
+ }
+ $destTitle->invalidateCache(); // update histories
+ # Check if this did anything
+ if( !$count ) {
+ $wgOut->addWikiMsg('mergehistory-fail');
+ return false;
+ }
+ # Update our logs
+ $log = new LogPage( 'merge' );
+ $log->addEntry( 'merge', $targetTitle, $this->mComment,
+ array($destTitle->getPrefixedText(),$TimestampLimit) );
+
+ $wgOut->addHtml( wfMsgExt( 'mergehistory-success', array('parseinline'),
+ $targetTitle->getPrefixedText(), $destTitle->getPrefixedText(), $count ) );
+
+ wfRunHooks( 'ArticleMergeComplete', array( $targetTitle, $destTitle ) );
+
+ return true;
+ }
+}
+
+class MergeHistoryPager extends ReverseChronologicalPager {
+ public $mForm, $mConds;
+
+ function __construct( $form, $conds = array(), $source, $dest ) {
+ $this->mForm = $form;
+ $this->mConds = $conds;
+ $this->title = $source;
+ $this->articleID = $source->getArticleID();
+
+ $dbr = wfGetDB( DB_SLAVE );
+ $maxtimestamp = $dbr->selectField( 'revision', 'MIN(rev_timestamp)',
+ array('rev_page' => $dest->getArticleID() ),
+ __METHOD__ );
+ $this->maxTimestamp = $maxtimestamp;
+
+ parent::__construct();
+ }
+
+ function getStartBody() {
+ wfProfileIn( __METHOD__ );
+ # Do a link batch query
+ $this->mResult->seek( 0 );
+ $batch = new LinkBatch();
+ # Give some pointers to make (last) links
+ $this->mForm->prevId = array();
+ while( $row = $this->mResult->fetchObject() ) {
+ $batch->addObj( Title::makeTitleSafe( NS_USER, $row->rev_user_text ) );
+ $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->rev_user_text ) );
+
+ $rev_id = isset($rev_id) ? $rev_id : $row->rev_id;
+ if( $rev_id > $row->rev_id )
+ $this->mForm->prevId[$rev_id] = $row->rev_id;
+ else if( $rev_id < $row->rev_id )
+ $this->mForm->prevId[$row->rev_id] = $rev_id;
+
+ $rev_id = $row->rev_id;
+ }
+
+ $batch->execute();
+ $this->mResult->seek( 0 );
+
+ wfProfileOut( __METHOD__ );
+ return '';
+ }
+
+ function formatRow( $row ) {
+ $block = new Block;
+ return $this->mForm->formatRevisionRow( $row );
+ }
+
+ function getQueryInfo() {
+ $conds = $this->mConds;
+ $conds['rev_page'] = $this->articleID;
+ $conds[] = "rev_timestamp < {$this->maxTimestamp}";
+
+ return array(
+ 'tables' => array('revision'),
+ '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
+ );
+ }
+
+ function getIndexField() {
+ return 'rev_timestamp';
+ }
+}
diff --git a/includes/specials/SpecialMostcategories.php b/includes/specials/SpecialMostcategories.php
new file mode 100644
index 00000000..e6810999
--- /dev/null
+++ b/includes/specials/SpecialMostcategories.php
@@ -0,0 +1,58 @@
+<?php
+/**
+ * @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
+ */
+
+/**
+ * implements Special:Mostcategories
+ * @ingroup SpecialPage
+ */
+class MostcategoriesPage extends QueryPage {
+
+ function getName() { return 'Mostcategories'; }
+ function isExpensive() { return true; }
+ function isSyndicated() { return false; }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ list( $categorylinks, $page) = $dbr->tableNamesN( 'categorylinks', 'page' );
+ return
+ "
+ SELECT
+ 'Mostcategories' as type,
+ page_namespace as namespace,
+ page_title as title,
+ COUNT(*) as value
+ FROM $categorylinks
+ LEFT JOIN $page ON cl_from = page_id
+ WHERE page_namespace = " . NS_MAIN . "
+ GROUP BY page_namespace, page_title
+ HAVING COUNT(*) > 1
+ ";
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgLang;
+ $title = Title::makeTitleSafe( $result->namespace, $result->title );
+ if ( !$title instanceof Title ) { throw new MWException('Invalid title in database'); }
+ $count = wfMsgExt( 'ncategories', array( 'parsemag', 'escape' ), $wgLang->formatNum( $result->value ) );
+ $link = $skin->makeKnownLinkObj( $title, $title->getText() );
+ return wfSpecialList( $link, $count );
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialMostcategories() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $wpp = new MostcategoriesPage();
+
+ $wpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialMostimages.php b/includes/specials/SpecialMostimages.php
new file mode 100644
index 00000000..6cfeb7ad
--- /dev/null
+++ b/includes/specials/SpecialMostimages.php
@@ -0,0 +1,54 @@
+<?php
+/**
+ * @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
+ */
+
+/**
+ * implements Special:Mostimages
+ * @ingroup SpecialPage
+ */
+class MostimagesPage extends ImageQueryPage {
+
+ function getName() { return 'Mostimages'; }
+ function isExpensive() { return true; }
+ function isSyndicated() { return false; }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ $imagelinks = $dbr->tableName( 'imagelinks' );
+ return
+ "
+ SELECT
+ 'Mostimages' as type,
+ " . NS_IMAGE . " as namespace,
+ il_to as title,
+ COUNT(*) as value
+ FROM $imagelinks
+ GROUP BY il_to
+ HAVING COUNT(*) > 1
+ ";
+ }
+
+ function getCellHtml( $row ) {
+ global $wgLang;
+ return wfMsgExt( 'nlinks', array( 'parsemag', 'escape' ),
+ $wgLang->formatNum( $row->value ) ) . '<br />';
+ }
+
+}
+
+/**
+ * Constructor
+ */
+function wfSpecialMostimages() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $wpp = new MostimagesPage();
+
+ $wpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialMostlinked.php b/includes/specials/SpecialMostlinked.php
new file mode 100644
index 00000000..078489bd
--- /dev/null
+++ b/includes/specials/SpecialMostlinked.php
@@ -0,0 +1,95 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * A special page to show pages ordered by the number of pages linking to them.
+ * Implements Special:Mostlinked
+ *
+ * @ingroup SpecialPage
+ *
+ * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
+ * @author Rob Church <robchur@gmail.com>
+ * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason
+ * @copyright © 2006 Rob Church
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ */
+class MostlinkedPage extends QueryPage {
+
+ function getName() { return 'Mostlinked'; }
+ function isExpensive() { return true; }
+ function isSyndicated() { return false; }
+
+ /**
+ * Note: Getting page_namespace only works if $this->isCached() is false
+ */
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ list( $pagelinks, $page ) = $dbr->tableNamesN( 'pagelinks', 'page' );
+ return
+ "SELECT 'Mostlinked' AS type,
+ pl_namespace AS namespace,
+ pl_title AS title,
+ COUNT(*) AS value,
+ page_namespace
+ FROM $pagelinks
+ LEFT JOIN $page ON pl_namespace=page_namespace AND pl_title=page_title
+ GROUP BY pl_namespace, pl_title, page_namespace
+ HAVING COUNT(*) > 1";
+ }
+
+ /**
+ * Pre-fill the link cache
+ */
+ function preprocessResults( $db, $res ) {
+ if( $db->numRows( $res ) > 0 ) {
+ $linkBatch = new LinkBatch();
+ while( $row = $db->fetchObject( $res ) )
+ $linkBatch->add( $row->namespace, $row->title );
+ $db->dataSeek( $res, 0 );
+ $linkBatch->execute();
+ }
+ }
+
+ /**
+ * Make a link to "what links here" for the specified title
+ *
+ * @param $title Title being queried
+ * @param $skin Skin to use
+ * @return string
+ */
+ function makeWlhLink( &$title, $caption, &$skin ) {
+ $wlh = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedDBkey() );
+ return $skin->makeKnownLinkObj( $wlh, $caption );
+ }
+
+ /**
+ * Make links to the page corresponding to the item, and the "what links here" page for it
+ *
+ * @param $skin Skin to be used
+ * @param $result Result row
+ * @return string
+ */
+ function formatResult( $skin, $result ) {
+ global $wgLang;
+ $title = Title::makeTitleSafe( $result->namespace, $result->title );
+ $link = $skin->makeLinkObj( $title );
+ $wlh = $this->makeWlhLink( $title,
+ wfMsgExt( 'nlinks', array( 'parsemag', 'escape'),
+ $wgLang->formatNum( $result->value ) ), $skin );
+ return wfSpecialList( $link, $wlh );
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialMostlinked() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $wpp = new MostlinkedPage();
+
+ $wpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialMostlinkedcategories.php b/includes/specials/SpecialMostlinkedcategories.php
new file mode 100644
index 00000000..ab250675
--- /dev/null
+++ b/includes/specials/SpecialMostlinkedcategories.php
@@ -0,0 +1,78 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * A querypage to show categories ordered in descending order by the pages in them
+ *
+ * @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
+ */
+class MostlinkedCategoriesPage extends QueryPage {
+
+ function getName() { return 'Mostlinkedcategories'; }
+ function isExpensive() { return true; }
+ function isSyndicated() { return false; }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ $categorylinks = $dbr->tableName( 'categorylinks' );
+ $name = $dbr->addQuotes( $this->getName() );
+ return
+ "
+ SELECT
+ $name as type,
+ " . NS_CATEGORY . " as namespace,
+ cl_to as title,
+ COUNT(*) as value
+ FROM $categorylinks
+ GROUP BY cl_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 = $skin->makeLinkObj( $nt, htmlspecialchars( $text ) );
+
+ $nlinks = wfMsgExt( 'nmembers', array( 'parsemag', 'escape'),
+ $wgLang->formatNum( $result->value ) );
+ return wfSpecialList($plink, $nlinks);
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialMostlinkedCategories() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $wpp = new MostlinkedCategoriesPage();
+
+ $wpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialMostlinkedtemplates.php b/includes/specials/SpecialMostlinkedtemplates.php
new file mode 100644
index 00000000..d597a4e0
--- /dev/null
+++ b/includes/specials/SpecialMostlinkedtemplates.php
@@ -0,0 +1,132 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Special page lists templates with a large number of
+ * transclusion links, i.e. "most used" templates
+ *
+ * @ingroup SpecialPage
+ * @author Rob Church <robchur@gmail.com>
+ */
+class SpecialMostlinkedtemplates extends QueryPage {
+
+ /**
+ * Name of the report
+ *
+ * @return string
+ */
+ public function getName() {
+ return 'Mostlinkedtemplates';
+ }
+
+ /**
+ * Is this report expensive, i.e should it be cached?
+ *
+ * @return bool
+ */
+ public function isExpensive() {
+ return true;
+ }
+
+ /**
+ * Is there a feed available?
+ *
+ * @return bool
+ */
+ public function isSyndicated() {
+ return false;
+ }
+
+ /**
+ * Sort the results in descending order?
+ *
+ * @return bool
+ */
+ public function sortDescending() {
+ return true;
+ }
+
+ /**
+ * Generate SQL for the report
+ *
+ * @return string
+ */
+ public function getSql() {
+ $dbr = wfGetDB( DB_SLAVE );
+ $templatelinks = $dbr->tableName( 'templatelinks' );
+ $name = $dbr->addQuotes( $this->getName() );
+ return "SELECT {$name} AS type,
+ " . NS_TEMPLATE . " AS namespace,
+ tl_title AS title,
+ COUNT(*) AS value
+ FROM {$templatelinks}
+ WHERE tl_namespace = " . NS_TEMPLATE . "
+ GROUP BY tl_title";
+ }
+
+ /**
+ * Pre-cache page existence to speed up link generation
+ *
+ * @param Database $dbr Database connection
+ * @param int $res Result pointer
+ */
+ public function preprocessResults( $db, $res ) {
+ $batch = new LinkBatch();
+ while( $row = $db->fetchObject( $res ) ) {
+ $batch->add( $row->namespace, $row->title );
+ }
+ $batch->execute();
+ if( $db->numRows( $res ) > 0 )
+ $db->dataSeek( $res, 0 );
+ }
+
+ /**
+ * Format a result row
+ *
+ * @param Skin $skin Skin to use for UI elements
+ * @param object $result Result row
+ * @return string
+ */
+ public function formatResult( $skin, $result ) {
+ $title = Title::makeTitleSafe( $result->namespace, $result->title );
+ if( $title instanceof Title ) {
+ return wfSpecialList(
+ $skin->makeLinkObj( $title ),
+ $this->makeWlhLink( $title, $skin, $result )
+ );
+ } else {
+ $tsafe = htmlspecialchars( $result->title );
+ return "Invalid title in result set; {$tsafe}";
+ }
+ }
+
+ /**
+ * Make a "what links here" link for a given title
+ *
+ * @param Title $title Title to make the link for
+ * @param Skin $skin Skin to use
+ * @param object $result Result row
+ * @return string
+ */
+ private function makeWlhLink( $title, $skin, $result ) {
+ global $wgLang;
+ $wlh = SpecialPage::getTitleFor( 'Whatlinkshere' );
+ $label = wfMsgExt( 'nlinks', array( 'parsemag', 'escape' ),
+ $wgLang->formatNum( $result->value ) );
+ return $skin->makeKnownLinkObj( $wlh, $label, 'target=' . $title->getPrefixedUrl() );
+ }
+}
+
+/**
+ * Execution function
+ *
+ * @param mixed $par Parameters passed to the page
+ */
+function wfSpecialMostlinkedtemplates( $par = false ) {
+ list( $limit, $offset ) = wfCheckLimits();
+ $mlt = new SpecialMostlinkedtemplates();
+ $mlt->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialMostrevisions.php b/includes/specials/SpecialMostrevisions.php
new file mode 100644
index 00000000..f5a0f8c0
--- /dev/null
+++ b/includes/specials/SpecialMostrevisions.php
@@ -0,0 +1,64 @@
+<?php
+/**
+ * A special page to show pages in the
+ *
+ * @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
+ */
+
+/**
+ * @ingroup SpecialPage
+ */
+class MostrevisionsPage extends QueryPage {
+
+ function getName() { return 'Mostrevisions'; }
+ function isExpensive() { return true; }
+ function isSyndicated() { return false; }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ list( $revision, $page ) = $dbr->tableNamesN( 'revision', 'page' );
+ return
+ "
+ SELECT
+ 'Mostrevisions' as type,
+ page_namespace as namespace,
+ page_title as title,
+ COUNT(*) as value
+ FROM $revision
+ JOIN $page ON page_id = rev_page
+ WHERE page_namespace = " . NS_MAIN . "
+ GROUP BY page_namespace, page_title
+ HAVING COUNT(*) > 1
+ ";
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgLang, $wgContLang;
+
+ $nt = Title::makeTitle( $result->namespace, $result->title );
+ $text = $wgContLang->convert( $nt->getPrefixedText() );
+
+ $plink = $skin->makeKnownLinkObj( $nt, $text );
+
+ $nl = wfMsgExt( 'nrevisions', array( 'parsemag', 'escape'),
+ $wgLang->formatNum( $result->value ) );
+ $nlink = $skin->makeKnownLinkObj( $nt, $nl, 'action=history' );
+
+ return wfSpecialList($plink, $nlink);
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialMostrevisions() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $wpp = new MostrevisionsPage();
+
+ $wpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialMovepage.php b/includes/specials/SpecialMovepage.php
new file mode 100644
index 00000000..efd2dcfd
--- /dev/null
+++ b/includes/specials/SpecialMovepage.php
@@ -0,0 +1,452 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Constructor
+ */
+function wfSpecialMovepage( $par = null ) {
+ global $wgUser, $wgOut, $wgRequest, $action;
+
+ # Check for database lock
+ if ( wfReadOnly() ) {
+ $wgOut->readOnlyPage();
+ return;
+ }
+
+ $target = isset( $par ) ? $par : $wgRequest->getVal( 'target' );
+ $oldTitleText = $wgRequest->getText( 'wpOldTitle', $target );
+ $newTitleText = $wgRequest->getText( 'wpNewTitle' );
+
+ $oldTitle = Title::newFromText( $oldTitleText );
+ $newTitle = Title::newFromText( $newTitleText );
+
+ if( is_null( $oldTitle ) ) {
+ $wgOut->showErrorPage( 'notargettitle', 'notargettext' );
+ return;
+ }
+ if( !$oldTitle->exists() ) {
+ $wgOut->showErrorPage( 'nopagetitle', 'nopagetext' );
+ return;
+ }
+
+ # Check rights
+ $permErrors = $oldTitle->getUserPermissionsErrors( 'move', $wgUser );
+ if( !empty( $permErrors ) ) {
+ $wgOut->showPermissionsErrorPage( $permErrors );
+ return;
+ }
+
+ $form = new MovePageForm( $oldTitle, $newTitle );
+
+ if ( 'submit' == $action && $wgRequest->wasPosted()
+ && $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) {
+ $form->doSubmit();
+ } else {
+ $form->showForm( '' );
+ }
+}
+
+/**
+ * HTML form for Special:Movepage
+ * @ingroup SpecialPage
+ */
+class MovePageForm {
+ var $oldTitle, $newTitle, $reason; # Text input
+ var $moveTalk, $deleteAndMove, $moveSubpages, $fixRedirects;
+
+ private $watch = false;
+
+ function MovePageForm( $oldTitle, $newTitle ) {
+ global $wgRequest;
+ $target = isset($par) ? $par : $wgRequest->getVal( 'target' );
+ $this->oldTitle = $oldTitle;
+ $this->newTitle = $newTitle;
+ $this->reason = $wgRequest->getText( 'wpReason' );
+ if ( $wgRequest->wasPosted() ) {
+ $this->moveTalk = $wgRequest->getBool( 'wpMovetalk', false );
+ $this->fixRedirects = $wgRequest->getBool( 'wpFixRedirects', false );
+ } else {
+ $this->moveTalk = $wgRequest->getBool( 'wpMovetalk', true );
+ $this->fixRedirects = $wgRequest->getBool( 'wpFixRedirects', 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;
+
+ $skin = $wgUser->getSkin();
+
+ $oldTitleLink = $skin->makeLinkObj( $this->oldTitle );
+ $oldTitle = $this->oldTitle->getPrefixedText();
+
+ $wgOut->setPagetitle( wfMsg( 'move-page', $oldTitle ) );
+ $wgOut->setSubtitle( wfMsg( 'move-page-backlink', $oldTitleLink ) );
+
+ if( $this->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->newTitle;
+ }
+
+ if ( $err == 'articleexists' && $wgUser->isAllowed( 'delete' ) ) {
+ $wgOut->addWikiMsg( 'delete_and_move_text', $newTitle );
+ $movepagebtn = wfMsg( 'delete_and_move' );
+ $submitVar = 'wpDeleteAndMove';
+ $confirm = "
+ <tr>
+ <td></td>
+ <td class='mw-input'>" .
+ Xml::checkLabel( wfMsg( 'delete_and_move_confirm' ), 'wpConfirm', 'wpConfirm' ) .
+ "</td>
+ </tr>";
+ $err = '';
+ } else {
+ $wgOut->addWikiMsg( 'movepagetext' );
+ $movepagebtn = wfMsg( 'movepagebtn' );
+ $submitVar = 'wpMove';
+ $confirm = false;
+ }
+
+ $oldTalk = $this->oldTitle->getTalkPage();
+ $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 ( $considerTalk ) {
+ $wgOut->addWikiMsg( 'movepagetalktext' );
+ }
+
+ $titleObj = SpecialPage::getTitleFor( 'Movepage' );
+ $token = htmlspecialchars( $wgUser->editToken() );
+
+ if ( $err != '' ) {
+ $wgOut->setSubtitle( wfMsg( 'formerror' ) );
+ if( $err == 'hookaborted' ) {
+ $errMsg = "<p><strong class=\"error\">$hookErr</strong></p>\n";
+ $wgOut->addHTML( $errMsg );
+ } else {
+ $wgOut->wrapWikiMsg( '<p><strong class="error">$1</strong></p>', $err );
+ }
+ }
+
+ $wgOut->addHTML(
+ Xml::openElement( 'form', array( 'method' => 'post', 'action' => $titleObj->getLocalURL( 'action=submit' ), 'id' => 'movepage' ) ) .
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', null, wfMsg( 'move-page-legend' ) ) .
+ Xml::openElement( 'table', array( 'border' => '0', 'id' => 'mw-movepage-table' ) ) .
+ "<tr>
+ <td class='mw-label'>" .
+ wfMsgHtml( 'movearticle' ) .
+ "</td>
+ <td class='mw-input'>
+ <strong>{$oldTitleLink}</strong>
+ </td>
+ </tr>
+ <tr>
+ <td class='mw-label'>" .
+ Xml::label( wfMsg( 'newtitle' ), 'wpNewTitle' ) .
+ "</td>
+ <td class='mw-input'>" .
+ Xml::input( 'wpNewTitle', 40, $newTitle, array( 'type' => 'text', 'id' => 'wpNewTitle' ) ) .
+ Xml::hidden( 'wpOldTitle', $oldTitle ) .
+ "</td>
+ </tr>
+ <tr>
+ <td class='mw-label'>" .
+ Xml::label( wfMsg( 'movereason' ), 'wpReason' ) .
+ "</td>
+ <td class='mw-input'>" .
+ Xml::tags( 'textarea', array( 'name' => 'wpReason', 'id' => 'wpReason', 'cols' => 60, 'rows' => 2 ), htmlspecialchars( $this->reason ) ) .
+ "</td>
+ </tr>"
+ );
+
+ if( $considerTalk ) {
+ $wgOut->addHTML( "
+ <tr>
+ <td></td>
+ <td class='mw-input'>" .
+ Xml::checkLabel( wfMsg( 'movetalk' ), 'wpMovetalk', 'wpMovetalk', $this->moveTalk ) .
+ "</td>
+ </tr>"
+ );
+ }
+
+ if ( $hasRedirects ) {
+ $wgOut->addHTML( "
+ <tr>
+ <td></td>
+ <td class='mw-input' >" .
+ Xml::checkLabel( wfMsg( 'fix-double-redirects' ), 'wpFixRedirects',
+ 'wpFixRedirects', $this->fixRedirects ) .
+ "</td>
+ </td>"
+ );
+ }
+
+ if( ($this->oldTitle->hasSubpages() || $this->oldTitle->getTalkPage()->hasSubpages())
+ && $this->oldTitle->userCan( 'move-subpages' ) ) {
+ $wgOut->addHTML( "
+ <tr>
+ <td></td>
+ <td class=\"mw-input\">" .
+ Xml::checkLabel( wfMsgHtml(
+ $this->oldTitle->hasSubpages()
+ ? 'move-subpages'
+ : 'move-talk-subpages'
+ ),
+ 'wpMovesubpages', 'wpMovesubpages',
+ # Don't check the box if we only have talk subpages to
+ # move and we aren't moving the talk page.
+ $this->moveSubpages && ($this->oldTitle->hasSubpages() || $this->moveTalk)
+ ) .
+ "</td>
+ </tr>"
+ );
+ }
+
+ $watchChecked = $this->watch || $wgUser->getBoolOption( 'watchmoves' )
+ || $this->oldTitle->userIsWatching();
+ $wgOut->addHTML( "
+ <tr>
+ <td></td>
+ <td class='mw-input'>" .
+ Xml::checkLabel( wfMsg( 'move-watch' ), 'wpWatch', 'watch', $watchChecked ) .
+ "</td>
+ </tr>
+ {$confirm}
+ <tr>
+ <td>&nbsp;</td>
+ <td class='mw-submit'>" .
+ Xml::submitButton( $movepagebtn, array( 'name' => $submitVar ) ) .
+ "</td>
+ </tr>" .
+ Xml::closeElement( 'table' ) .
+ Xml::hidden( 'wpEditToken', $token ) .
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' ) .
+ "\n"
+ );
+
+ $this->showLogFragment( $this->oldTitle, $wgOut );
+
+ }
+
+ function doSubmit() {
+ global $wgOut, $wgUser, $wgRequest, $wgMaximumMovedPages, $wgLang;
+
+ if ( $wgUser->pingLimiter( 'move' ) ) {
+ $wgOut->rateLimited();
+ return;
+ }
+
+ $ot = $this->oldTitle;
+ $nt = $this->newTitle;
+
+ # Delete to make way if requested
+ if ( $wgUser->isAllowed( 'delete' ) && $this->deleteAndMove ) {
+ $article = new Article( $nt );
+
+ # Disallow deletions of big articles
+ $bigHistory = $article->isBigDeletion();
+ if( $bigHistory && !$nt->userCan( 'bigdelete' ) ) {
+ global $wgLang, $wgDeleteRevisionsLimit;
+ $this->showForm( array('delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ) );
+ return;
+ }
+
+ // This may output an error message and exit
+ $article->doDelete( wfMsgForContent( 'delete_and_move_reason' ) );
+ }
+
+ # don't allow moving to pages with # in
+ if ( !$nt || $nt->getFragment() != '' ) {
+ $this->showForm( 'badtitletext' );
+ return;
+ }
+
+ $error = $ot->moveTo( $nt, true, $this->reason );
+ if ( $error !== true ) {
+ # FIXME: showForm() should handle multiple errors
+ call_user_func_array(array($this, 'showForm'), $error[0]);
+ return;
+ }
+
+ if ( $this->fixRedirects ) {
+ DoubleRedirectJob::fixRedirects( 'move', $ot, $nt );
+ }
+
+ wfRunHooks( 'SpecialMovepageAfterMove', array( &$this , &$ot , &$nt ) ) ;
+
+ $wgOut->setPagetitle( wfMsg( 'pagemovedsub' ) );
+
+ $oldUrl = $ot->getFullUrl( 'redirect=no' );
+ $newUrl = $nt->getFullUrl();
+ $oldText = $ot->getPrefixedText();
+ $newText = $nt->getPrefixedText();
+ $oldLink = "<span class='plainlinks'>[$oldUrl $oldText]</span>";
+ $newLink = "<span class='plainlinks'>[$newUrl $newText]</span>";
+
+ $wgOut->addWikiMsg( 'movepage-moved', $oldLink, $newLink, $oldText, $newText );
+
+ # 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
+ # can't move any talk pages: cancel that.
+ if( $ot->isTalkPage() || $nt->isTalkPage() ) {
+ $this->moveTalk = false;
+ }
+
+ if( !$ot->userCan( 'move-subpages' ) ) {
+ $this->moveSubpages = false;
+ }
+
+ # Next make a list of id's. This might be marginally less efficient
+ # than a more direct method, but this is not a highly performance-cri-
+ # tical code path and readable code is more important here.
+ #
+ # Note: this query works nicely on MySQL 5, but the optimizer in MySQL
+ # 4 might get confused. If so, consider rewriting as a UNION.
+ #
+ # If the target namespace doesn't allow subpages, moving with subpages
+ # would mean that you couldn't move them back in one operation, which
+ # is bad. FIXME: A specific error message should be given in this
+ # case.
+ $dbr = wfGetDB( DB_MASTER );
+ if( $this->moveSubpages && (
+ MWNamespace::hasSubpages( $nt->getNamespace() ) || (
+ $this->moveTalk &&
+ MWNamespace::hasSubpages( $nt->getTalkPage()->getNamespace() )
+ )
+ ) ) {
+ $conds = array(
+ 'page_title LIKE '.$dbr->addQuotes( $dbr->escapeLike( $ot->getDBkey() ) . '/%' )
+ .' OR page_title = ' . $dbr->addQuotes( $ot->getDBkey() )
+ );
+ $conds['page_namespace'] = array();
+ if( MWNamespace::hasSubpages( $nt->getNamespace() ) ) {
+ $conds['page_namespace'] []= $ot->getNamespace();
+ }
+ if( $this->moveTalk && MWNamespace::hasSubpages( $nt->getTalkPage()->getNamespace() ) ) {
+ $conds['page_namespace'] []= $ot->getTalkPage()->getNamespace();
+ }
+ } elseif( $this->moveTalk ) {
+ $conds = array(
+ 'page_namespace' => $ot->getTalkPage()->getNamespace(),
+ 'page_title' => $ot->getDBKey()
+ );
+ } else {
+ # Skip the query
+ $conds = null;
+ }
+
+ $extrapages = array();
+ if( !is_null( $conds ) ) {
+ $extrapages = $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() ) {
+ # Already did this one.
+ continue;
+ }
+
+ $oldSubpage = Title::newFromRow( $row );
+ $newPageName = preg_replace(
+ '#^'.preg_quote( $ot->getDBKey(), '#' ).'#',
+ $nt->getDBKey(),
+ $oldSubpage->getDBKey()
+ );
+ if( $oldSubpage->isTalkPage() ) {
+ $newNs = $nt->getTalkPage()->getNamespace();
+ } else {
+ $newNs = $nt->getSubjectPage()->getNamespace();
+ }
+ # Bug 14385: we need makeTitleSafe because the new page names may
+ # be longer than 255 characters.
+ $newSubpage = Title::makeTitleSafe( $newNs, $newPageName );
+ if( !$newSubpage ) {
+ $oldLink = $skin->makeKnownLinkObj( $oldSubpage );
+ $extraOutput []= wfMsgHtml( 'movepage-page-unmoved', $oldLink,
+ htmlspecialchars(Title::makeName( $newNs, $newPageName )));
+ continue;
+ }
+
+ # This was copy-pasted from Renameuser, bleh.
+ if ( $newSubpage->exists() && !$oldSubpage->isValidMoveTarget( $newSubpage ) ) {
+ $link = $skin->makeKnownLinkObj( $newSubpage );
+ $extraOutput []= wfMsgHtml( 'movepage-page-exists', $link );
+ } else {
+ $success = $oldSubpage->moveTo( $newSubpage, true, $this->reason );
+ if( $success === true ) {
+ if ( $this->fixRedirects ) {
+ DoubleRedirectJob::fixRedirects( 'move', $oldSubpage, $newSubpage );
+ }
+ $oldLink = $skin->makeKnownLinkObj( $oldSubpage, '', 'redirect=no' );
+ $newLink = $skin->makeKnownLinkObj( $newSubpage );
+ $extraOutput []= wfMsgHtml( 'movepage-page-moved', $oldLink, $newLink );
+ } else {
+ $oldLink = $skin->makeKnownLinkObj( $oldSubpage );
+ $newLink = $skin->makeLinkObj( $newSubpage );
+ $extraOutput []= wfMsgHtml( 'movepage-page-unmoved', $oldLink, $newLink );
+ }
+ }
+
+ ++$count;
+ if( $count >= $wgMaximumMovedPages ) {
+ $extraOutput []= wfMsgExt( 'movepage-max-pages', array( 'parsemag', 'escape' ), $wgLang->formatNum( $wgMaximumMovedPages ) );
+ break;
+ }
+ }
+
+ if( $extraOutput !== array() ) {
+ $wgOut->addHTML( "<ul>\n<li>" . implode( "</li>\n<li>", $extraOutput ) . "</li>\n</ul>" );
+ }
+
+ # Deal with watches (we don't watch subpages)
+ if( $this->watch ) {
+ $wgUser->addWatch( $ot );
+ $wgUser->addWatch( $nt );
+ } else {
+ $wgUser->removeWatch( $ot );
+ $wgUser->removeWatch( $nt );
+ }
+ }
+
+ function showLogFragment( $title, &$out ) {
+ $out->addHTML( Xml::element( 'h2', NULL, LogPage::logName( 'move' ) ) );
+ LogEventsList::showLogExtract( $out, 'move', $title->getPrefixedText() );
+ }
+
+}
diff --git a/includes/specials/SpecialNewimages.php b/includes/specials/SpecialNewimages.php
new file mode 100644
index 00000000..e57f6fc1
--- /dev/null
+++ b/includes/specials/SpecialNewimages.php
@@ -0,0 +1,211 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ * FIXME: this code is crap, should use Pager and Database::select().
+ */
+
+/**
+ *
+ */
+function wfSpecialNewimages( $par, $specialPage ) {
+ global $wgUser, $wgOut, $wgLang, $wgRequest, $wgGroupPermissions, $wgMiserMode;
+
+ $wpIlMatch = $wgRequest->getText( 'wpIlMatch' );
+ $dbr = wfGetDB( DB_SLAVE );
+ $sk = $wgUser->getSkin();
+ $shownav = !$specialPage->including();
+ $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 not bot groups, do not set $hidebotsql */
+ if ($botconds) {
+ $isbotmember=$dbr->makeList($botconds, LIST_OR);
+
+ /** This join, in conjunction with WHERE ug_group
+ IS NULL, returns only those rows from IMAGE
+ where the uploading user is not a member of
+ a group which has the 'bot' permission set.
+ */
+ $ug = $dbr->tableName('user_groups');
+ $hidebotsql = " LEFT OUTER JOIN $ug ON img_user=ug_user AND ($isbotmember)";
+ }
+ }
+
+ $image = $dbr->tableName('image');
+
+ $sql="SELECT img_timestamp from $image";
+ if ($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];
+ } else {
+ $ts=false;
+ }
+ $dbr->freeResult($res);
+ $sql='';
+
+ /** If we were clever, we'd use this to cache. */
+ $latestTimestamp = wfTimestamp( TS_MW, $ts);
+
+ /** Hardcode this for now. */
+ $limit = 48;
+
+ if ( $parval = intval( $par ) ) {
+ if ( $parval <= $limit && $parval > 0 ) {
+ $limit = $parval;
+ }
+ }
+
+ $where = array();
+ $searchpar = '';
+ if ( $wpIlMatch != '' && !$wgMiserMode) {
+ $nt = Title::newFromUrl( $wpIlMatch );
+ if($nt ) {
+ $m = $dbr->strencode( strtolower( $nt->getDBkey() ) );
+ $m = str_replace( '%', "\\%", $m );
+ $m = str_replace( '_', "\\_", $m );
+ $where[] = "LOWER(img_name) LIKE '%{$m}%'";
+ $searchpar = '&wpIlMatch=' . urlencode( $wpIlMatch );
+ }
+ }
+
+ $invertSort = false;
+ if( $until = $wgRequest->getVal( 'until' ) ) {
+ $where[] = "img_timestamp < '" . $dbr->timestamp( $until ) . "'";
+ }
+ if( $from = $wgRequest->getVal( 'from' ) ) {
+ $where[] = "img_timestamp >= '" . $dbr->timestamp( $from ) . "'";
+ $invertSort = true;
+ }
+ $sql='SELECT img_size, img_name, img_user, img_user_text,'.
+ "img_description,img_timestamp FROM $image";
+
+ if($hidebotsql) {
+ $sql .= $hidebotsql;
+ $where[]='ug_group IS NULL';
+ }
+ 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');
+
+ /**
+ * We have to flip things around to get the last N after a certain date
+ */
+ $images = array();
+ while ( $s = $dbr->fetchObject( $res ) ) {
+ if( $invertSort ) {
+ array_unshift( $images, $s );
+ } else {
+ array_push( $images, $s );
+ }
+ }
+ $dbr->freeResult( $res );
+
+ $gallery = new ImageGallery();
+ $firstTimestamp = null;
+ $lastTimestamp = null;
+ $shownImages = 0;
+ foreach( $images as $s ) {
+ if( ++$shownImages > $limit ) {
+ # One extra just to test for whether to show a page link;
+ # don't actually show it.
+ break;
+ }
+
+ $name = $s->img_name;
+ $ut = $s->img_user_text;
+
+ $nt = Title::newFromText( $name, NS_IMAGE );
+ $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" );
+
+ $timestamp = wfTimestamp( TS_MW, $s->img_timestamp );
+ if( empty( $firstTimestamp ) ) {
+ $firstTimestamp = $timestamp;
+ }
+ $lastTimestamp = $timestamp;
+ }
+
+ $bydate = wfMsg( 'bydate' );
+ $lt = $wgLang->formatNum( min( $shownImages, $limit ) );
+ 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';
+ } else {
+ $botpar='';
+ }
+ $now = wfTimestampNow();
+ $d = $wgLang->date( $now, true );
+ $t = $wgLang->time( $now, true );
+ $dateLink = $sk->makeKnownLinkObj( $titleObj, wfMsgHtml( 'sp-newimages-showfrom', $d, $t ),
+ 'from='.$now.$botpar.$searchpar );
+
+ $botLink = $sk->makeKnownLinkObj($titleObj, wfMsgHtml( 'showhidebots',
+ ($hidebots ? wfMsgHtml('show') : wfMsgHtml('hide'))),'hidebots='.($hidebots ? '0' : '1').$searchpar);
+
+
+ $opts = array( 'parsemag', 'escapenoentities' );
+ $prevLink = wfMsgExt( 'prevn', $opts, $wgLang->formatNum( $limit ) );
+ if( $firstTimestamp && $firstTimestamp != $latestTimestamp ) {
+ $prevLink = $sk->makeKnownLinkObj( $titleObj, $prevLink, 'from=' . $firstTimestamp . $botpar . $searchpar );
+ }
+
+ $nextLink = wfMsgExt( 'nextn', $opts, $wgLang->formatNum( $limit ) );
+ if( $shownImages > $limit && $lastTimestamp ) {
+ $nextLink = $sk->makeKnownLinkObj( $titleObj, $nextLink, 'until=' . $lastTimestamp.$botpar.$searchpar );
+ }
+
+ $prevnext = '<p>' . $botLink . ' '. wfMsgHtml( 'viewprevnext', $prevLink, $nextLink, $dateLink ) .'</p>';
+
+ if ($shownav)
+ $wgOut->addHTML( $prevnext );
+
+ if( count( $images ) ) {
+ $wgOut->addHTML( $gallery->toHTML() );
+ if ($shownav)
+ $wgOut->addHTML( $prevnext );
+ } else {
+ $wgOut->addWikiMsg( 'noimages' );
+ }
+}
diff --git a/includes/specials/SpecialNewpages.php b/includes/specials/SpecialNewpages.php
new file mode 100644
index 00000000..1a410ae0
--- /dev/null
+++ b/includes/specials/SpecialNewpages.php
@@ -0,0 +1,437 @@
+<?php
+
+/**
+ * implements Special:Newpages
+ * @ingroup SpecialPage
+ */
+class SpecialNewpages extends SpecialPage {
+
+ // Stored objects
+ protected $opts, $skin;
+
+ // Some internal settings
+ protected $showNavigation = false;
+
+ public function __construct(){
+ parent::__construct( 'Newpages' );
+ $this->includable( true );
+ }
+
+ protected function setup( $par ) {
+ global $wgRequest, $wgUser, $wgEnableNewpagesUserFilter;
+
+ // Options
+ $opts = new FormOptions();
+ $this->opts = $opts; // bind
+ $opts->add( 'hideliu', false );
+ $opts->add( 'hidepatrolled', false );
+ $opts->add( 'hidebots', false );
+ $opts->add( 'limit', 50 );
+ $opts->add( 'offset', '' );
+ $opts->add( 'namespace', '0' );
+ $opts->add( 'username', '' );
+ $opts->add( 'feed', '' );
+
+ // Set values
+ $opts->fetchValuesFromRequest( $wgRequest );
+ if ( $par ) $this->parseParams( $par );
+
+ // Validate
+ $opts->validateIntBounds( 'limit', 0, 5000 );
+ if( !$wgEnableNewpagesUserFilter ) {
+ $opts->setValue( 'username', '' );
+ }
+
+ // Store some objects
+ $this->skin = $wgUser->getSkin();
+ }
+
+ protected function parseParams( $par ) {
+ global $wgLang;
+ $bits = preg_split( '/\s*,\s*/', trim( $par ) );
+ foreach ( $bits as $bit ) {
+ if ( 'shownav' == $bit )
+ $this->showNavigation = true;
+ if ( 'hideliu' === $bit )
+ $this->opts->setValue( 'hideliu', true );
+ if ( 'hidepatrolled' == $bit )
+ $this->opts->setValue( 'hidepatrolled', true );
+ if ( 'hidebots' == $bit )
+ $this->opts->setValue( 'hidebots', true );
+ if ( is_numeric( $bit ) )
+ $this->opts->setValue( 'limit', intval( $bit ) );
+
+ $m = array();
+ if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) )
+ $this->opts->setValue( 'limit', intval($m[1]) );
+ // PG offsets not just digits!
+ if ( preg_match( '/^offset=([^=]+)$/', $bit, $m ) )
+ $this->opts->setValue( 'offset', intval($m[1]) );
+ if ( preg_match( '/^namespace=(.*)$/', $bit, $m ) ) {
+ $ns = $wgLang->getNsIndex( $m[1] );
+ if( $ns !== false ) {
+ $this->opts->setValue( 'namespace', $ns );
+ }
+ }
+ }
+ }
+
+ /**
+ * Show a form for filtering namespace and username
+ *
+ * @param string $par
+ * @return string
+ */
+ public function execute( $par ) {
+ global $wgLang, $wgGroupPermissions, $wgUser, $wgOut;
+
+ $this->setHeaders();
+ $this->outputHeader();
+
+ $this->showNavigation = !$this->including(); // Maybe changed in setup
+ $this->setup( $par );
+
+ if( !$this->including() ) {
+ // Settings
+ $this->form();
+
+ $this->setSyndicated();
+ $feedType = $this->opts->getValue( 'feed' );
+ if( $feedType ) {
+ return $this->feed( $feedType );
+ }
+ }
+
+ $pager = new NewPagesPager( $this, $this->opts );
+ $pager->mLimit = $this->opts->getValue( 'limit' );
+ $pager->mOffset = $this->opts->getValue( 'offset' );
+
+ if( $pager->getNumRows() ) {
+ $navigation = '';
+ if ( $this->showNavigation ) $navigation = $pager->getNavigationBar();
+ $wgOut->addHTML( $navigation . $pager->getBody() . $navigation );
+ } else {
+ $wgOut->addWikiMsg( 'specialpage-empty' );
+ }
+ }
+
+ protected function filterLinks() {
+ global $wgGroupPermissions, $wgUser;
+
+ // show/hide links
+ $showhide = array( wfMsgHtml( 'show' ), wfMsgHtml( 'hide' ) );
+
+ // Option value -> message mapping
+ $filters = array(
+ 'hideliu' => 'rcshowhideliu',
+ 'hidepatrolled' => 'rcshowhidepatr',
+ 'hidebots' => 'rcshowhidebots'
+ );
+
+ // Disable some if needed
+ if ( $wgGroupPermissions['*']['createpage'] !== true )
+ unset($filters['hideliu']);
+
+ if ( !$wgUser->useNPPatrol() )
+ unset($filters['hidepatrolled']);
+
+ $links = array();
+ $changed = $this->opts->getChangedValues();
+ unset($changed['offset']); // Reset offset if query type changes
+
+ $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 )
+ );
+ $links[$key] = wfMsgHtml( $msg, $link );
+ }
+
+ return implode( ' | ', $links );
+ }
+
+ protected function form() {
+ global $wgOut, $wgEnableNewpagesUserFilter, $wgScript;
+
+ // Consume values
+ $this->opts->consumeValue( 'offset' ); // don't carry offset, DWIW
+ $namespace = $this->opts->consumeValue( 'namespace' );
+ $username = $this->opts->consumeValue( 'username' );
+
+ // Check username input validity
+ $ut = Title::makeTitleSafe( NS_USER, $username );
+ $userText = $ut ? $ut->getText() : '';
+
+ // Store query values in hidden fields so that form submission doesn't lose them
+ $hidden = array();
+ foreach ( $this->opts->getUnconsumedValues() as $key => $value ) {
+ $hidden[] = Xml::hidden( $key, $value );
+ }
+ $hidden = implode( "\n", $hidden );
+
+ $form = Xml::openElement( 'form', array( 'action' => $wgScript ) ) .
+ Xml::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) .
+ Xml::fieldset( wfMsg( 'newpages' ) ) .
+ Xml::openElement( 'table', array( 'id' => 'mw-newpages-table' ) ) .
+ "<tr>
+ <td class='mw-label'>" .
+ Xml::label( wfMsg( 'namespace' ), 'namespace' ) .
+ "</td>
+ <td class='mw-input'>" .
+ Xml::namespaceSelector( $namespace, 'all' ) .
+ "</td>
+ </tr>" .
+ ($wgEnableNewpagesUserFilter ?
+ "<tr>
+ <td class='mw-label'>" .
+ Xml::label( wfMsg( 'newpages-username' ), 'mw-np-username' ) .
+ "</td>
+ <td class='mw-input'>" .
+ Xml::input( 'username', 30, $userText, array( 'id' => 'mw-np-username' ) ) .
+ "</td>
+ </tr>" : "" ) .
+ "<tr> <td></td>
+ <td class='mw-submit'>" .
+ Xml::submitButton( wfMsg( 'allpagessubmit' ) ) .
+ "</td>
+ </tr>" .
+ "<tr>
+ <td></td>
+ <td class='mw-input'>" .
+ $this->filterLinks() .
+ "</td>
+ </tr>" .
+ Xml::closeElement( 'table' ) .
+ Xml::closeElement( 'fieldset' ) .
+ $hidden .
+ Xml::closeElement( 'form' );
+
+ $wgOut->addHTML( $form );
+ }
+
+ protected function setSyndicated() {
+ global $wgOut;
+ $queryParams = array(
+ 'namespace' => $this->opts->getValue( 'namespace' ),
+ 'username' => $this->opts->getValue( 'username' )
+ );
+ $wgOut->setSyndicated( true );
+ $wgOut->setFeedAppendQuery( wfArrayToCGI( $queryParams ) );
+ }
+
+ /**
+ * Format a row, providing the timestamp, links to the page/history, size, user links, and a comment
+ *
+ * @param $skin Skin to use
+ * @param $result Result row
+ * @return string
+ */
+ public function formatRow( $result ) {
+ global $wgLang, $wgContLang, $wgUser;
+ $dm = $wgContLang->getDirMark();
+
+ $title = Title::makeTitleSafe( $result->page_namespace, $result->page_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' );
+ $length = wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ),
+ $wgLang->formatNum( $result->length ) );
+ $ulink = $this->skin->userLink( $result->rc_user, $result->rc_user_text ) . ' ' .
+ $this->skin->userToolLinks( $result->rc_user, $result->rc_user_text );
+ $comment = $this->skin->commentBlock( $result->rc_comment );
+ $css = $this->patrollable( $result ) ? " class='not-patrolled'" : '';
+
+ return "<li{$css}>{$time} {$dm}{$plink} ({$hist}) {$dm}[{$length}] {$dm}{$ulink} {$comment}</li>\n";
+ }
+
+ /**
+ * Should a specific result row provide "patrollable" links?
+ *
+ * @param $result Result row
+ * @return bool
+ */
+ protected function patrollable( $result ) {
+ global $wgUser;
+ return ( $wgUser->useNPPatrol() && !$result->rc_patrolled );
+ }
+
+ /**
+ * Output a subscription feed listing recent edits to this page.
+ * @param string $type
+ */
+ protected function feed( $type ) {
+ global $wgFeed, $wgFeedClasses;
+
+ 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(),
+ wfMsg( 'tagline' ),
+ $this->getTitle()->getFullUrl() );
+
+ $pager = new NewPagesPager( $this, $this->opts );
+ $limit = $this->opts->getValue( 'limit' );
+ global $wgFeedLimit;
+ if( $limit > $wgFeedLimit ) {
+ $limit = $wgFeedLimit;
+ }
+ $pager->mLimit = $limit;
+
+ $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( 'Newpages' );
+ $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->rc_timestamp;
+ $comments = $title->getTalkPage()->getFullURL();
+
+ return new FeedItem(
+ $title->getPrefixedText(),
+ $this->feedItemDesc( $row ),
+ $title->getFullURL(),
+ $date,
+ $this->feedItemAuthor( $row ),
+ $comments);
+ } else {
+ return NULL;
+ }
+ }
+
+ /**
+ * 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 : '';
+ }
+
+ protected function feedItemDesc( $row ) {
+ $revision = Revision::newFromId( $row->rev_id );
+ if( $revision ) {
+ return '<p>' . htmlspecialchars( $revision->getUserText() ) . ': ' .
+ htmlspecialchars( $revision->getComment() ) .
+ "</p>\n<hr />\n<div>" .
+ nl2br( htmlspecialchars( $revision->getText() ) ) . "</div>";
+ }
+ return '';
+ }
+}
+
+/**
+ * @ingroup SpecialPage Pager
+ */
+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(){
+ static $title = null;
+ if ( $title === null )
+ $title = $this->mForm->getTitle();
+ return $title;
+ }
+
+ function getQueryInfo() {
+ global $wgEnableNewpagesUserFilter, $wgGroupPermissions, $wgUser;
+ $conds = array();
+ $conds['rc_new'] = 1;
+
+ $namespace = $this->opts->getValue( 'namespace' );
+ $namespace = ( $namespace === 'all' ) ? false : intval( $namespace );
+
+ $username = $this->opts->getValue( 'username' );
+ $user = Title::makeTitleSafe( NS_USER, $username );
+
+ if( $namespace !== false ) {
+ $conds['page_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();
+ $rcIndexes = 'rc_user_text';
+ # If anons cannot make new pages, don't "exclude logged in users"!
+ } elseif( $wgGroupPermissions['*']['createpage'] && $this->opts->getValue( 'hideliu' ) ) {
+ $conds['rc_user'] = 0;
+ }
+ # If this user cannot see patrolled edits or they are off, don't do dumb queries!
+ if( $this->opts->getValue( 'hidepatrolled' ) && $wgUser->useNPPatrol() ) {
+ $conds['rc_patrolled'] = 0;
+ }
+ if( $this->opts->getValue( 'hidebots' ) ) {
+ $conds['rc_bot'] = 0;
+ }
+
+ return array(
+ 'tables' => array( 'recentchanges', 'page' ),
+ 'fields' => 'page_namespace,page_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) )
+ );
+ }
+
+ function getIndexField() {
+ return 'rc_timestamp';
+ }
+
+ function formatRow( $row ) {
+ return $this->mForm->formatRow( $row );
+ }
+
+ function getStartBody() {
+ # Do a batch existence check on pages
+ $linkBatch = new LinkBatch();
+ 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->execute();
+ return "<ul>";
+ }
+
+ function getEndBody() {
+ return "</ul>";
+ }
+}
diff --git a/includes/specials/SpecialPopularpages.php b/includes/specials/SpecialPopularpages.php
new file mode 100644
index 00000000..eb572736
--- /dev/null
+++ b/includes/specials/SpecialPopularpages.php
@@ -0,0 +1,67 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * implements Special:Popularpages
+ * @ingroup SpecialPage
+ */
+class PopularPagesPage extends QueryPage {
+
+ function getName() {
+ return "Popularpages";
+ }
+
+ function isExpensive() {
+ # page_counter is not indexed
+ return true;
+ }
+ function isSyndicated() { return false; }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ $page = $dbr->tableName( 'page' );
+
+ $query =
+ "SELECT 'Popularpages' as type,
+ page_namespace as namespace,
+ page_title as title,
+ page_counter as value
+ FROM $page ";
+ $where =
+ "WHERE page_is_redirect=0 AND page_namespace";
+
+ global $wgContentNamespaces;
+ if( empty( $wgContentNamespaces ) ) {
+ $where .= '='.NS_MAIN;
+ } else if( count( $wgContentNamespaces ) > 1 ) {
+ $where .= ' in (' . implode( ', ', $wgContentNamespaces ) . ')';
+ } else {
+ $where .= '='.$wgContentNamespaces[0];
+ }
+
+ return $query . $where;
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgLang, $wgContLang;
+ $title = Title::makeTitle( $result->namespace, $result->title );
+ $link = $skin->makeKnownLinkObj( $title, htmlspecialchars( $wgContLang->convert( $title->getPrefixedText() ) ) );
+ $nv = wfMsgExt( 'nviews', array( 'parsemag', 'escape'),
+ $wgLang->formatNum( $result->value ) );
+ return wfSpecialList($link, $nv);
+ }
+}
+
+/**
+ * Constructor
+ */
+function wfSpecialPopularpages() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $ppp = new PopularPagesPage();
+
+ return $ppp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialPreferences.php b/includes/specials/SpecialPreferences.php
new file mode 100644
index 00000000..b3468a3c
--- /dev/null
+++ b/includes/specials/SpecialPreferences.php
@@ -0,0 +1,1126 @@
+<?php
+/**
+ * Hold things related to displaying and saving user preferences.
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Entry point that create the "Preferences" object
+ */
+function wfSpecialPreferences() {
+ global $wgRequest;
+
+ $form = new PreferencesForm( $wgRequest );
+ $form->execute();
+}
+
+/**
+ * Preferences form handling
+ * This object will show the preferences form and can save it as well.
+ * @ingroup SpecialPage
+ */
+class PreferencesForm {
+ var $mQuickbar, $mOldpass, $mNewpass, $mRetypePass, $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 $mUnderline, $mWatchlistEdits;
+
+ /**
+ * Constructor
+ * Load some values
+ */
+ function PreferencesForm( &$request ) {
+ 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->mMath = $request->getVal( 'wpMath' );
+ $this->mDate = $request->getVal( 'wpDate' );
+ $this->mUserEmail = $request->getVal( 'wpUserEmail' );
+ $this->mRealName = $wgAllowRealName ? $request->getVal( 'wpRealName' ) : '';
+ $this->mEmailFlag = $request->getCheck( 'wpEmailFlag' ) ? 0 : 1;
+ $this->mNick = $request->getVal( 'wpNick' );
+ $this->mUserLanguage = $request->getVal( 'wpUserLanguage' );
+ $this->mUserVariant = $request->getVal( 'wpUserVariant' );
+ $this->mSearch = $request->getVal( 'wpSearch' );
+ $this->mRecent = $request->getVal( 'wpRecent' );
+ $this->mRecentDays = $request->getVal( 'wpRecentDays' );
+ $this->mHourDiff = $request->getVal( 'wpHourDiff' );
+ $this->mSearchLines = $request->getVal( 'wpSearchLines' );
+ $this->mSearchChars = $request->getVal( 'wpSearchChars' );
+ $this->mImageSize = $request->getVal( 'wpImageSize' );
+ $this->mThumbSize = $request->getInt( 'wpThumbSize' );
+ $this->mUnderline = $request->getInt( 'wpOpunderline' );
+ $this->mAction = $request->getVal( 'action' );
+ $this->mReset = $request->getCheck( 'wpReset' );
+ $this->mPosted = $request->wasPosted();
+ $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' ) &&
+ $this->mPosted &&
+ $wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) );
+
+ # User toggles (the big ugly unsorted list of checkboxes)
+ $this->mToggles = array();
+ if ( $this->mPosted ) {
+ $togs = User::getToggles();
+ foreach ( $togs as $tname ) {
+ $this->mToggles[$tname] = $request->getCheck( "wpOp$tname" ) ? 1 : 0;
+ }
+ }
+
+ $this->mUsedToggles = array();
+
+ # Search namespace options
+ # Note: namespaces don't necessarily have consecutive keys
+ $this->mSearchNs = array();
+ if ( $this->mPosted ) {
+ $namespaces = $wgContLang->getNamespaces();
+ foreach ( $namespaces as $i => $namespace ) {
+ if ( $i >= 0 ) {
+ $this->mSearchNs[$i] = $request->getCheck( "wpNs$i" ) ? 1 : 0;
+ }
+ }
+ }
+
+ # Validate language
+ if ( !preg_match( '/^[a-z\-]*$/', $this->mUserLanguage ) ) {
+ $this->mUserLanguage = 'nolanguage';
+ }
+
+ wfRunHooks( 'InitPreferencesForm', array( $this, $request ) );
+ }
+
+ function execute() {
+ global $wgUser, $wgOut;
+
+ if ( $wgUser->isAnon() ) {
+ $wgOut->showErrorPage( 'prefsnologin', 'prefsnologintext' );
+ return;
+ }
+ if ( wfReadOnly() ) {
+ $wgOut->readOnlyPage();
+ return;
+ }
+ if ( $this->mReset ) {
+ $this->resetPrefs();
+ $this->mainPrefsForm( 'reset', wfMsg( 'prefsreset' ) );
+ } else if ( $this->mSaveprefs ) {
+ $this->savePreferences();
+ } else {
+ $this->resetPrefs();
+ $this->mainPrefsForm( '' );
+ }
+ }
+ /**
+ * @access private
+ */
+ function validateInt( &$val, $min=0, $max=0x7fffffff ) {
+ $val = intval($val);
+ $val = min($val, $max);
+ $val = max($val, $min);
+ return $val;
+ }
+
+ /**
+ * @access private
+ */
+ function validateFloat( &$val, $min, $max=0x7fffffff ) {
+ $val = floatval( $val );
+ $val = min( $val, $max );
+ $val = max( $val, $min );
+ return( $val );
+ }
+
+ /**
+ * @access private
+ */
+ function validateIntOrNull( &$val, $min=0, $max=0x7fffffff ) {
+ $val = trim($val);
+ if($val === '') {
+ return null;
+ } else {
+ return $this->validateInt( $val, $min, $max );
+ }
+ }
+
+ /**
+ * @access private
+ */
+ function validateDate( $val ) {
+ global $wgLang, $wgContLang;
+ if ( $val !== false && (
+ in_array( $val, (array)$wgLang->getDatePreferences() ) ||
+ in_array( $val, (array)$wgContLang->getDatePreferences() ) ) )
+ {
+ return $val;
+ } else {
+ return $wgLang->getDefaultDateFormat();
+ }
+ }
+
+ /**
+ * 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',
+ * @access private
+ * @param string $s the user input
+ * @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 );
+ }
+ return $s;
+ }
+
+ /**
+ * @access private
+ */
+ function savePreferences() {
+ global $wgUser, $wgOut, $wgParser;
+ global $wgEnableUserEmail, $wgEnableEmail;
+ 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;
+
+ if( $wgUser->getOption( 'language' ) !== $this->mUserLanguage ) {
+ $needRedirect = true;
+ } else {
+ $needRedirect = false;
+ }
+
+ # Validate the signature and clean it up as needed
+ global $wgMaxSigChars;
+ if( mb_strlen( $this->mNick ) > $wgMaxSigChars ) {
+ global $wgLang;
+ $this->mainPrefsForm( 'error',
+ wfMsgExt( 'badsiglength', 'parsemag', $wgLang->formatNum( $wgMaxSigChars ) ) );
+ return;
+ } elseif( $this->mToggles['fancysig'] ) {
+ if( $wgParser->validateSig( $this->mNick ) !== false ) {
+ $this->mNick = $wgParser->cleanSig( $this->mNick );
+ } else {
+ $this->mainPrefsForm( 'error', wfMsg( 'badsig' ) );
+ return;
+ }
+ } else {
+ // When no fancy sig used, make sure ~{3,5} get removed.
+ $this->mNick = $wgParser->cleanSigInSig( $this->mNick );
+ }
+
+ $wgUser->setOption( 'language', $this->mUserLanguage );
+ $wgUser->setOption( 'variant', $this->mUserVariant );
+ $wgUser->setOption( 'nickname', $this->mNick );
+ $wgUser->setOption( 'quickbar', $this->mQuickbar );
+ $wgUser->setOption( 'skin', $this->mSkin );
+ global $wgUseTeX;
+ if( $wgUseTeX ) {
+ $wgUser->setOption( 'math', $this->mMath );
+ }
+ $wgUser->setOption( 'date', $this->validateDate( $this->mDate ) );
+ $wgUser->setOption( 'searchlimit', $this->validateIntOrNull( $this->mSearch ) );
+ $wgUser->setOption( 'contextlines', $this->validateIntOrNull( $this->mSearchLines ) );
+ $wgUser->setOption( 'contextchars', $this->validateIntOrNull( $this->mSearchChars ) );
+ $wgUser->setOption( 'rclimit', $this->validateIntOrNull( $this->mRecent ) );
+ $wgUser->setOption( 'rcdays', $this->validateInt($this->mRecentDays, 1, ceil($wgRCMaxAge / (3600*24))));
+ $wgUser->setOption( 'wllimit', $this->validateIntOrNull( $this->mWatchlistEdits, 0, 1000 ) );
+ $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( '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
+ foreach( $this->mSearchNs as $i => $value ) {
+ $wgUser->setOption( "searchNs{$i}", $value );
+ }
+
+ if( $wgEnableEmail && $wgEnableUserEmail ) {
+ $wgUser->setOption( 'disablemail', $this->mEmailFlag );
+ }
+
+ # Set user toggles
+ foreach ( $this->mToggles as $tname => $tvalue ) {
+ $wgUser->setOption( $tname, $tvalue );
+ }
+
+ $error = false;
+ if( $wgEnableEmail ) {
+ $newadr = $this->mUserEmail;
+ $oldadr = $wgUser->getEmail();
+ if( ($newadr != '') && ($newadr != $oldadr) ) {
+ # the user has supplied a new email address on the login page
+ if( $wgUser->isValidEmailAddr( $newadr ) ) {
+ # new behaviour: set this new emailaddr from login-page into user database record
+ $wgUser->setEmail( $newadr );
+ # but flag as "dirty" = unauthenticated
+ $wgUser->invalidateEmail();
+ if ($wgEmailAuthentication) {
+ # Mail a temporary password to the dirty address.
+ # User can come back through the confirmation URL to re-enable email.
+ $result = $wgUser->sendConfirmationMail();
+ if( WikiError::isError( $result ) ) {
+ $error = wfMsg( 'mailerror', htmlspecialchars( $result->getMessage() ) );
+ } else {
+ $error = wfMsg( 'eauthentsent', $wgUser->getName() );
+ }
+ }
+ } else {
+ $error = wfMsg( 'invalidemailaddress' );
+ }
+ } else {
+ if( $wgEmailConfirmToEdit && empty( $newadr ) ) {
+ $this->mainPrefsForm( 'error', wfMsg( 'noemailtitle' ) );
+ return;
+ }
+ $wgUser->setEmail( $this->mUserEmail );
+ }
+ if( $oldadr != $newadr ) {
+ wfRunHooks( 'PrefsEmailAudit', array( $wgUser, $oldadr, $newadr ) );
+ }
+ }
+
+ if( !$wgAuth->updateExternalDB( $wgUser ) ){
+ $this->mainPrefsForm( 'error', wfMsg( 'externaldberror' ) );
+ return;
+ }
+
+ $msg = '';
+ if ( !wfRunHooks( 'SavePreferences', array( $this, $wgUser, &$msg, $oldOptions ) ) ) {
+ $this->mainPrefsForm( 'error', $msg );
+ return;
+ }
+
+ $wgUser->setCookies();
+ $wgUser->saveSettings();
+
+ if( $needRedirect && $error === false ) {
+ $title = SpecialPage::getTitleFor( 'Preferences' );
+ $wgOut->redirect( $title->getFullURL( 'success' ) );
+ return;
+ }
+
+ $wgOut->parserOptions( ParserOptions::newFromUser( $wgUser ) );
+ $this->mainPrefsForm( $error === false ? 'success' : 'error', $error);
+ }
+
+ /**
+ * @access private
+ */
+ function resetPrefs() {
+ global $wgUser, $wgLang, $wgContLang, $wgContLanguageCode, $wgAllowRealName;
+
+ $this->mOldpass = $this->mNewpass = $this->mRetypePass = '';
+ $this->mUserEmail = $wgUser->getEmail();
+ $this->mUserEmailAuthenticationtimestamp = $wgUser->getEmailAuthenticationtimestamp();
+ $this->mRealName = ($wgAllowRealName) ? $wgUser->getRealName() : '';
+
+ # language value might be blank, default to content language
+ $this->mUserLanguage = $wgUser->getOption( 'language', $wgContLanguageCode );
+
+ $this->mUserVariant = $wgUser->getOption( 'variant');
+ $this->mEmailFlag = $wgUser->getOption( 'disablemail' ) == 1 ? 1 : 0;
+ $this->mNick = $wgUser->getOption( 'nickname' );
+
+ $this->mQuickbar = $wgUser->getOption( 'quickbar' );
+ $this->mSkin = Skin::normalizeKey( $wgUser->getOption( 'skin' ) );
+ $this->mMath = $wgUser->getOption( 'math' );
+ $this->mDate = $wgUser->getDatePreference();
+ $this->mRows = $wgUser->getOption( 'rows' );
+ $this->mCols = $wgUser->getOption( 'cols' );
+ $this->mStubs = $wgUser->getOption( 'stubthreshold' );
+ $this->mHourDiff = $wgUser->getOption( 'timecorrection' );
+ $this->mSearch = $wgUser->getOption( 'searchlimit' );
+ $this->mSearchLines = $wgUser->getOption( 'contextlines' );
+ $this->mSearchChars = $wgUser->getOption( 'contextchars' );
+ $this->mImageSize = $wgUser->getOption( 'imagesize' );
+ $this->mThumbSize = $wgUser->getOption( 'thumbsize' );
+ $this->mRecent = $wgUser->getOption( 'rclimit' );
+ $this->mRecentDays = $wgUser->getOption( 'rcdays' );
+ $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();
+ foreach ( $togs as $tname ) {
+ $this->mToggles[$tname] = $wgUser->getOption( $tname );
+ }
+
+ $namespaces = $wgContLang->getNamespaces();
+ foreach ( $namespaces as $i => $namespace ) {
+ if ( $i >= NS_MAIN ) {
+ $this->mSearchNs[$i] = $wgUser->getOption( 'searchNs'.$i );
+ }
+ }
+
+ wfRunHooks( 'ResetPreferences', array( $this, $wgUser ) );
+ }
+
+ /**
+ * @access private
+ */
+ function namespacesCheckboxes() {
+ global $wgContLang;
+
+ # Determine namespace checkboxes
+ $namespaces = $wgContLang->getNamespaces();
+ $r1 = null;
+
+ foreach ( $namespaces as $i => $name ) {
+ if ($i < 0)
+ continue;
+ $checked = $this->mSearchNs[$i] ? "checked='checked'" : '';
+ $name = str_replace( '_', ' ', $namespaces[$i] );
+
+ if ( empty($name) )
+ $name = wfMsg( 'blanknamespace' );
+
+ $r1 .= "<input type='checkbox' value='1' name='wpNs$i' id='wpNs$i' {$checked}/> <label for='wpNs$i'>{$name}</label><br />\n";
+ }
+ return $r1;
+ }
+
+
+ function getToggle( $tname, $trailer = false, $disabled = false ) {
+ global $wgUser, $wgLang;
+
+ $this->mUsedToggles[$tname] = true;
+ $ttext = $wgLang->getUserToggle( $tname );
+
+ $checked = $wgUser->getOption( $tname ) == 1 ? ' checked="checked"' : '';
+ $disabled = $disabled ? ' disabled="disabled"' : '';
+ $trailer = $trailer ? $trailer : '';
+ return "<div class='toggle'><input type='checkbox' value='1' id=\"$tname\" name=\"wpOp$tname\"$checked$disabled />" .
+ " <span class='toggletext'><label for=\"$tname\">$ttext</label>$trailer</span></div>\n";
+ }
+
+ function getToggles( $items ) {
+ $out = "";
+ foreach( $items as $item ) {
+ if( $item === false )
+ continue;
+ if( is_array( $item ) ) {
+ list( $key, $trailer ) = $item;
+ } else {
+ $key = $item;
+ $trailer = false;
+ }
+ $out .= $this->getToggle( $key, $trailer );
+ }
+ return $out;
+ }
+
+ function addRow($td1, $td2) {
+ return "<tr><td class='mw-label'>$td1</td><td class='mw-input'>$td2</td></tr>";
+ }
+
+ /**
+ * Helper function for user information panel
+ * @param $td1 label for an item
+ * @param $td2 item or null
+ * @param $td3 optional help or null
+ * @return xhtml block
+ */
+ function tableRow( $td1, $td2 = null, $td3 = null ) {
+
+ if ( is_null( $td3 ) ) {
+ $td3 = '';
+ } else {
+ $td3 = Xml::tags( 'tr', null,
+ Xml::tags( 'td', array( 'class' => 'pref-label', 'colspan' => '2' ), $td3 )
+ );
+ }
+
+ if ( is_null( $td2 ) ) {
+ $td1 = Xml::tags( 'td', array( 'class' => 'pref-label', 'colspan' => '2' ), $td1 );
+ $td2 = '';
+ } else {
+ $td1 = Xml::tags( 'td', array( 'class' => 'pref-label' ), $td1 );
+ $td2 = Xml::tags( 'td', array( 'class' => 'pref-input' ), $td2 );
+ }
+
+ return Xml::tags( 'tr', null, $td1 . $td2 ). $td3 . "\n";
+
+ }
+
+ /**
+ * @access private
+ */
+ function mainPrefsForm( $status , $message = '' ) {
+ global $wgUser, $wgOut, $wgLang, $wgContLang;
+ global $wgAllowRealName, $wgImageLimits, $wgThumbLimits;
+ global $wgDisableLangConversion;
+ global $wgEnotifWatchlist, $wgEnotifUserTalk,$wgEnotifMinorEdits;
+ global $wgRCShowWatchingUsers, $wgEnotifRevealEditorAddress;
+ global $wgEnableEmail, $wgEnableUserEmail, $wgEmailAuthentication;
+ global $wgContLanguageCode, $wgDefaultSkin, $wgSkipSkins, $wgAuth;
+ global $wgEmailConfirmToEdit, $wgAjaxSearch, $wgEnableMWSuggest;
+
+ $wgOut->setPageTitle( wfMsg( 'preferences' ) );
+ $wgOut->setArticleRelated( false );
+ $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->addScriptFile( 'prefs.js' );
+
+ $wgOut->disallowUserJs(); # Prevent hijacked user scripts from sniffing passwords etc.
+
+ if ( $this->mSuccess || 'success' == $status ) {
+ $wgOut->wrapWikiMsg( '<div class="successbox"><strong>$1</strong></div>', 'savedprefs' );
+ } else if ( 'error' == $status ) {
+ $wgOut->addWikiText( '<div class="errorbox"><strong>' . $message . '</strong></div>' );
+ } else if ( '' != $status ) {
+ $wgOut->addWikiText( $message . "\n----" );
+ }
+
+ $qbs = $wgLang->getQuickbarSettings();
+ $skinNames = $wgLang->getSkinNames();
+ $mathopts = $wgLang->getMathNames();
+ $dateopts = $wgLang->getDatePreferences();
+ $togs = User::getToggles();
+
+ $titleObj = SpecialPage::getTitleFor( 'Preferences' );
+
+ # Pre-expire some toggles so they won't show if disabled
+ $this->mUsedToggles[ 'shownumberswatching' ] = true;
+ $this->mUsedToggles[ 'showupdated' ] = true;
+ $this->mUsedToggles[ 'enotifwatchlistpages' ] = true;
+ $this->mUsedToggles[ 'enotifusertalkpages' ] = true;
+ $this->mUsedToggles[ 'enotifminoredits' ] = true;
+ $this->mUsedToggles[ 'enotifrevealaddr' ] = true;
+ $this->mUsedToggles[ 'ccmeonemails' ] = true;
+ $this->mUsedToggles[ 'uselivepreview' ] = true;
+
+
+ if ( !$this->mEmailFlag ) { $emfc = 'checked="checked"'; }
+ else { $emfc = ''; }
+
+
+ if ($wgEmailAuthentication && ($this->mUserEmail != '') ) {
+ if( $wgUser->getEmailAuthenticationTimestamp() ) {
+ $emailauthenticated = wfMsg('emailauthenticated',$wgLang->timeanddate($wgUser->getEmailAuthenticationTimestamp(), true ) ).'<br />';
+ $disableEmailPrefs = false;
+ } else {
+ $disableEmailPrefs = true;
+ $skin = $wgUser->getSkin();
+ $emailauthenticated = wfMsg('emailnotauthenticated').'<br />' .
+ $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Confirmemail' ),
+ wfMsg( 'emailconfirmlink' ) ) . '<br />';
+ }
+ } else {
+ $emailauthenticated = '';
+ $disableEmailPrefs = false;
+ }
+
+ if ($this->mUserEmail == '') {
+ $emailauthenticated = wfMsg( 'noemailprefs' ) . '<br />';
+ }
+
+ $ps = $this->namespacesCheckboxes();
+
+ $enotifwatchlistpages = ($wgEnotifWatchlist) ? $this->getToggle( 'enotifwatchlistpages', false, $disableEmailPrefs ) : '';
+ $enotifusertalkpages = ($wgEnotifUserTalk) ? $this->getToggle( 'enotifusertalkpages', false, $disableEmailPrefs ) : '';
+ $enotifminoredits = ($wgEnotifWatchlist && $wgEnotifMinorEdits) ? $this->getToggle( 'enotifminoredits', false, $disableEmailPrefs ) : '';
+ $enotifrevealaddr = (($wgEnotifWatchlist || $wgEnotifUserTalk) && $wgEnotifRevealEditorAddress) ? $this->getToggle( 'enotifrevealaddr', false, $disableEmailPrefs ) : '';
+
+ # </FIXME>
+
+ $wgOut->addHTML(
+ Xml::openElement( 'form', array(
+ 'action' => $titleObj->getLocalUrl(),
+ 'method' => 'post',
+ 'id' => 'mw-preferences-form',
+ ) ) .
+ Xml::openElement( 'div', array( 'id' => 'preferences' ) )
+ );
+
+ # User data
+
+ $wgOut->addHTML(
+ Xml::fieldset( wfMsg('prefs-personal') ) .
+ Xml::openElement( 'table' ) .
+ $this->tableRow( Xml::element( 'h2', null, wfMsg( 'prefs-personal' ) ) )
+ );
+
+ # Get groups to which the user belongs
+ $userEffectiveGroups = $wgUser->getEffectiveGroups();
+ $userEffectiveGroupsArray = array();
+ foreach( $userEffectiveGroups as $ueg ) {
+ if( $ueg == '*' ) {
+ // Skip the default * group, seems useless here
+ continue;
+ }
+ $userEffectiveGroupsArray[] = User::makeGroupLinkHTML( $ueg );
+ }
+ asort( $userEffectiveGroupsArray );
+
+ $sk = $wgUser->getSkin();
+ $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.
+ # $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(
+ wfMsgExt( 'prefs-memberingroups', array( 'parseinline' ), count( $userEffectiveGroupsArray ) ),
+ implode( wfMsg( 'comma-separator' ), $userEffectiveGroupsArray ) .
+ '<br />(' . implode( ' | ', $toolLinks ) . ')'
+ ) .
+
+ $this->tableRow(
+ wfMsgHtml( 'prefs-edits' ),
+ $wgLang->formatNum( User::edits( $wgUser->getId() ) )
+ );
+
+ if( wfRunHooks( 'PreferencesUserInformationPanel', array( $this, &$userInformationHtml ) ) ) {
+ $wgOut->addHtml( $userInformationHtml );
+ }
+
+ if ( $wgAllowRealName ) {
+ $wgOut->addHTML(
+ $this->tableRow(
+ Xml::label( wfMsg('yourrealname'), 'wpRealName' ),
+ Xml::input( 'wpRealName', 25, $this->mRealName, array( 'id' => 'wpRealName' ) ),
+ Xml::tags('div', array( 'class' => 'prefsectiontip' ),
+ wfMsgExt( 'prefs-help-realname', 'parseinline' )
+ )
+ )
+ );
+ }
+ if ( $wgEnableEmail ) {
+ $wgOut->addHTML(
+ $this->tableRow(
+ Xml::label( wfMsg('youremail'), 'wpUserEmail' ),
+ Xml::input( 'wpUserEmail', 25, $this->mUserEmail, array( 'id' => 'wpUserEmail' ) ),
+ Xml::tags('div', array( 'class' => 'prefsectiontip' ),
+ wfMsgExt( $wgEmailConfirmToEdit ? 'prefs-help-email-required' : 'prefs-help-email', 'parseinline' )
+ )
+ )
+ );
+ }
+
+ global $wgParser, $wgMaxSigChars;
+ if( mb_strlen( $this->mNick ) > $wgMaxSigChars ) {
+ $invalidSig = $this->tableRow(
+ '&nbsp;',
+ Xml::element( 'span', array( 'class' => 'error' ),
+ wfMsgExt( 'badsiglength', 'parsemag', $wgLang->formatNum( $wgMaxSigChars ) ) )
+ );
+ } elseif( !empty( $this->mToggles['fancysig'] ) &&
+ false === $wgParser->validateSig( $this->mNick ) ) {
+ $invalidSig = $this->tableRow(
+ '&nbsp;',
+ Xml::element( 'span', array( 'class' => 'error' ), wfMsg( 'badsig' ) )
+ );
+ } else {
+ $invalidSig = '';
+ }
+
+ $wgOut->addHTML(
+ $this->tableRow(
+ Xml::label( wfMsg( 'yournick' ), 'wpNick' ),
+ Xml::input( 'wpNick', 25, $this->mNick,
+ array(
+ 'id' => 'wpNick',
+ // Note: $wgMaxSigChars is enforced in Unicode characters,
+ // both on the backend and now in the browser.
+ // Badly-behaved requests may still try to submit
+ // an overlong string, however.
+ 'maxlength' => $wgMaxSigChars ) )
+ ) .
+ $invalidSig .
+ $this->tableRow( '&nbsp;', $this->getToggle( 'fancysig' ) )
+ );
+
+ list( $lsLabel, $lsSelect) = Xml::languageSelector( $this->mUserLanguage );
+ $wgOut->addHTML(
+ $this->tableRow( $lsLabel, $lsSelect )
+ );
+
+ /* see if there are multiple language variants to choose from*/
+ if(!$wgDisableLangConversion) {
+ $variants = $wgContLang->getVariants();
+ $variantArray = array();
+
+ $languages = Language::getLanguageNames( true );
+ foreach($variants as $v) {
+ $v = str_replace( '_', '-', strtolower($v));
+ if( array_key_exists( $v, $languages ) ) {
+ // If it doesn't have a name, we'll pretend it doesn't exist
+ $variantArray[$v] = $languages[$v];
+ }
+ }
+
+ $options = "\n";
+ foreach( $variantArray as $code => $name ) {
+ $selected = ($code == $this->mUserVariant);
+ $options .= Xml::option( "$code - $name", $code, $selected ) . "\n";
+ }
+
+ if(count($variantArray) > 1) {
+ $wgOut->addHtml(
+ $this->tableRow(
+ Xml::label( wfMsg( 'yourvariant' ), 'wpUserVariant' ),
+ Xml::tags( 'select',
+ array( 'name' => 'wpUserVariant', 'id' => 'wpUserVariant' ),
+ $options
+ )
+ )
+ );
+ }
+ }
+
+ # Password
+ if( $wgAuth->allowPasswordChange() ) {
+ $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" )
+ )
+ )
+ );
+ }
+
+ # <FIXME>
+ # Enotif
+ if ( $wgEnableEmail ) {
+
+ $moreEmail = '';
+ if ($wgEnableUserEmail) {
+ // fixme -- the "allowemail" pseudotoggle is a hacked-together
+ // inversion for the "disableemail" preference.
+ $emf = wfMsg( 'allowemail' );
+ $disabled = $disableEmailPrefs ? ' disabled="disabled"' : '';
+ $moreEmail =
+ "<input type='checkbox' $emfc $disabled value='1' name='wpEmailFlag' id='wpEmailFlag' /> <label for='wpEmailFlag'>$emf</label>" .
+ $this->getToggle( 'ccmeonemails', '', $disableEmailPrefs );
+ }
+
+
+ $wgOut->addHTML(
+ $this->tableRow( Xml::element( 'h2', null, wfMsg( 'email' ) ) ) .
+ $this->tableRow(
+ $emailauthenticated.
+ $enotifrevealaddr.
+ $enotifwatchlistpages.
+ $enotifusertalkpages.
+ $enotifminoredits.
+ $moreEmail
+ )
+ );
+ }
+ # </FIXME>
+
+ $wgOut->addHTML(
+ Xml::closeElement( 'table' ) .
+ Xml::closeElement( 'fieldset' )
+ );
+
+
+ # Quickbar
+ #
+ if ($this->mSkin == 'cologneblue' || $this->mSkin == 'standard') {
+ $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" );
+ } 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 ) );
+ }
+
+ # 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];
+ }
+ }
+ asort($validSkinNames);
+ foreach ($validSkinNames as $skinkey => $sn ) {
+ if ( in_array( $skinkey, $wgSkipSkins ) ) {
+ continue;
+ }
+ $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" );
+
+ # Math
+ #
+ global $wgUseTeX;
+ if( $wgUseTeX ) {
+ $wgOut->addHTML( "<fieldset>\n<legend>" . wfMsg('math') . '</legend>' );
+ foreach ( $mathopts as $k => $v ) {
+ $checked = ($k == $this->mMath);
+ $wgOut->addHTML(
+ Xml::openElement( 'div' ) .
+ Xml::radioLabel( wfMsg( $v ), 'wpMath', $k, "mw-sp-math-$k", $checked ) .
+ Xml::closeElement( 'div' ) . "\n"
+ );
+ }
+ $wgOut->addHTML( "</fieldset>\n\n" );
+ }
+
+ # Files
+ #
+ $wgOut->addHTML(
+ "<fieldset>\n" . Xml::element( 'legend', null, wfMsg( 'files' ) ) . "\n"
+ );
+
+ $imageLimitOptions = null;
+ foreach ( $wgImageLimits as $index => $limits ) {
+ $selected = ($index == $this->mImageSize);
+ $imageLimitOptions .= Xml::option( "{$limits[0]}×{$limits[1]}" .
+ 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);
+ $imageThumbOptions .= Xml::option($size . wfMsg('unit-pixel'), $index,
+ $selected);
+ }
+
+ $thumbSizeId = 'wpThumbSize';
+ $wgOut->addHTML(
+ "<div>" . Xml::label( wfMsg('thumbsize'), $thumbSizeId ) . " " .
+ Xml::openElement( 'select', array( 'name' => $thumbSizeId, 'id' => $thumbSizeId ) ) .
+ $imageThumbOptions .
+ Xml::closeElement( 'select' ) . "</div>\n"
+ );
+
+ $wgOut->addHTML( "</fieldset>\n\n" );
+
+ # Date format
+ #
+ # Date/Time
+ #
+
+ $wgOut->addHTML(
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', null, wfMsg( 'datetime' ) ) . "\n"
+ );
+
+ if ($dateopts) {
+ $wgOut->addHTML(
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', null, wfMsg( 'dateformat' ) ) . "\n"
+ );
+ $idCnt = 0;
+ $epoch = '20010115161234'; # Wikipedia day
+ foreach( $dateopts as $key ) {
+ if( $key == 'default' ) {
+ $formatted = wfMsg( 'datedefault' );
+ } else {
+ $formatted = $wgLang->timeanddate( $epoch, false, $key );
+ }
+ $wgOut->addHTML(
+ Xml::tags( 'div', null,
+ Xml::radioLabel( $formatted, 'wpDate', $key, "wpDate$idCnt", $key == $this->mDate )
+ ) . "\n"
+ );
+ $idCnt++;
+ }
+ $wgOut->addHTML( Xml::closeElement( 'fieldset' ) . "\n" );
+ }
+
+ $nowlocal = $wgLang->time( $now = wfTimestampNow(), true );
+ $nowserver = $wgLang->time( $now, false );
+
+ $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(
+ Xml::label( wfMsg( 'timezoneoffset' ), 'wpHourDiff' ),
+ Xml::input( 'wpHourDiff', 6, $this->mHourDiff, array( 'id' => 'wpHourDiff' ) ) ) .
+ "<tr>
+ <td></td>
+ <td class='mw-submit'>" .
+ Xml::element( 'input',
+ array( 'type' => 'button',
+ 'value' => wfMsg( 'guesstimezone' ),
+ 'onclick' => 'javascript:guessTimezone()',
+ 'id' => 'guesstimezonebutton',
+ 'style' => 'display:none;' ) ) .
+ "</td>
+ </tr>" .
+ Xml::closeElement( 'table' ) .
+ Xml::tags( 'div', array( 'class' => 'prefsectiontip' ), wfMsgExt( 'timezonetext', 'parseinline' ) ).
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'fieldset' ) . "\n\n"
+ );
+
+ # 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>" .
+ $this->getToggles( array(
+ 'editsection',
+ 'editsectiononrightclick',
+ 'editondblclick',
+ 'editwidth',
+ 'showtoolbar',
+ 'previewonfirst',
+ 'previewontop',
+ 'minordefault',
+ 'externaleditor',
+ '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( '<br />' );
+
+ $toggles[] = 'hideminor';
+ if( $wgRCShowWatchingUsers )
+ $toggles[] = 'shownumberswatching';
+ $toggles[] = 'usenewrc';
+ $wgOut->addHtml( $this->getToggles( $toggles ) );
+
+ $wgOut->addHtml( '</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( $this->getToggles( array( 'watchlisthideown', 'watchlisthidebots', 'watchlisthideminor' ) ) );
+
+ 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 ) );
+ }
+ $this->mUsedToggles['watchcreations'] = true;
+ $this->mUsedToggles['watchdefault'] = true;
+ $this->mUsedToggles['watchmoves'] = true;
+ $this->mUsedToggles['watchdeletion'] = true;
+
+ $wgOut->addHtml( '</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' ),
+ Xml::check( 'wpDisableMWSuggest', $this->mDisableMWSuggest, array( 'id' => 'wpDisableMWSuggest' ) )
+ ) : '';
+ $wgOut->addHTML(
+ // Elements for the search tab itself
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', null, wfMsg( 'searchresultshead' ) ) .
+ // Elements for the search options in the search tab
+ 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' ) )
+ ) .
+ $this->addRow(
+ Xml::label( wfMsg( 'contextlines' ), 'wpSearchLines' ),
+ Xml::input( 'wpSearchLines', 4, $this->mSearchLines, array( 'id' => 'wpSearchLines' ) )
+ ) .
+ $this->addRow(
+ Xml::label( wfMsg( 'contextchars' ), 'wpSearchChars' ),
+ Xml::input( 'wpSearchChars', 4, $this->mSearchChars, array( 'id' => 'wpSearchChars' ) )
+ ) .
+ $mwsuggest .
+ Xml::closeElement( 'table' ) .
+ Xml::closeElement( 'fieldset' ) .
+ // Elements for the namespace options in the search tab
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', null, wfMsg( 'prefs-namespaces' ) ) .
+ wfMsgExt( 'defaultns', array( 'parse' ) ) .
+ $ps .
+ Xml::closeElement( 'fieldset' ) .
+ // End of the search tab
+ Xml::closeElement( 'fieldset' )
+ );
+
+ # 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' ) ) );
+ $msgUnderline = htmlspecialchars( wfMsg ( 'tog-underline' ) );
+ $msgUnderlinenever = htmlspecialchars( wfMsg ( 'underline-never' ) );
+ $msgUnderlinealways = htmlspecialchars( wfMsg ( 'underline-always' ) );
+ $msgUnderlinedefault = htmlspecialchars( wfMsg ( 'underline-default' ) );
+ $uopt = $wgUser->getOption("underline");
+ $s0 = $uopt == 0 ? ' selected="selected"' : '';
+ $s1 = $uopt == 1 ? ' selected="selected"' : '';
+ $s2 = $uopt == 2 ? ' selected="selected"' : '';
+ $wgOut->addHTML("
+<div class='toggle'><p><label for='wpOpunderline'>$msgUnderline</label>
+<select name='wpOpunderline' id='wpOpunderline'>
+<option value=\"0\"$s0>$msgUnderlinenever</option>
+<option value=\"1\"$s1>$msgUnderlinealways</option>
+<option value=\"2\"$s2>$msgUnderlinedefault</option>
+</select></p></div>");
+
+ foreach ( $togs as $tname ) {
+ if( !array_key_exists( $tname, $this->mUsedToggles ) ) {
+ $wgOut->addHTML( $this->getToggle( $tname ) );
+ }
+ }
+ $wgOut->addHTML( '</fieldset>' );
+
+ wfRunHooks( 'RenderPreferencesForm', array( $this, $wgOut ) );
+
+ $token = htmlspecialchars( $wgUser->editToken() );
+ $skin = $wgUser->getSkin();
+ $wgOut->addHTML( "
+ <div id='prefsubmit'>
+ <div>
+ <input type='submit' name='wpSaveprefs' class='btnSavePrefs' value=\"" . wfMsgHtml( 'saveprefs' ) . '"'.$skin->tooltipAndAccesskey('save')." />
+ <input type='submit' name='wpReset' value=\"" . wfMsgHtml( 'resetprefs' ) . "\" />
+ </div>
+
+ </div>
+
+ <input type='hidden' name='wpEditToken' value=\"{$token}\" />
+ </div></form>\n" );
+
+ $wgOut->addHtml( Xml::tags( 'div', array( 'class' => "prefcache" ),
+ wfMsgExt( 'clearyourcache', 'parseinline' ) )
+ );
+ }
+}
diff --git a/includes/specials/SpecialPrefixindex.php b/includes/specials/SpecialPrefixindex.php
new file mode 100644
index 00000000..9c880349
--- /dev/null
+++ b/includes/specials/SpecialPrefixindex.php
@@ -0,0 +1,152 @@
+<?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
+ * @ingroup SpecialPage
+ */
+class SpecialPrefixindex extends SpecialAllpages {
+ // Inherit $maxPerPage
+
+ // Define other properties
+ protected $name = 'Prefixindex';
+ protected $nsfromMsg = 'allpagesprefix';
+
+ /**
+ * @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 ) {
+ global $wgOut, $wgUser, $wgContLang;
+
+ $fname = 'indexShowChunk';
+
+ $sk = $wgUser->getSkin();
+
+ if (!isset($from)) $from = $prefix;
+
+ $fromList = $this->getNamespaceKeyAndText($namespace, $from);
+ $prefixList = $this->getNamespaceKeyAndText($namespace, $prefix);
+ $namespaces = $wgContLang->getNamespaces();
+ $align = $wgContLang->isRtl() ? 'left' : 'right';
+
+ if ( !$prefixList || !$fromList ) {
+ $out = wfMsgWikiHtml( 'allpagesbadtitle' );
+ } elseif ( !in_array( $namespace, array_keys( $namespaces ) ) ) {
+ // Show errormessage and reset to NS_MAIN
+ $out = wfMsgExt( 'allpages-bad-ns', array( 'parseinline' ), $namespace );
+ $namespace = NS_MAIN;
+ } else {
+ list( $namespace, $prefixKey, $prefix ) = $prefixList;
+ list( /* $fromNs */, $fromKey, $from ) = $fromList;
+
+ ### FIXME: should complain if $fromNs != $namespace
+
+ $dbr = wfGetDB( DB_SLAVE );
+
+ $res = $dbr->select( 'page',
+ array( 'page_namespace', 'page_title', 'page_is_redirect' ),
+ array(
+ 'page_namespace' => $namespace,
+ 'page_title LIKE \'' . $dbr->escapeLike( $prefixKey ) .'%\'',
+ 'page_title >= ' . $dbr->addQuotes( $fromKey ),
+ ),
+ $fname,
+ array(
+ 'ORDER BY' => 'page_title',
+ 'LIMIT' => $this->maxPerPage + 1,
+ 'USE INDEX' => 'name_title',
+ )
+ );
+
+ ### FIXME: side link to previous
+
+ $n = 0;
+ 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>';
+ }
+ $out .= "<td>$link</td>";
+ $n++;
+ if( $n % 3 == 0 ) {
+ $out .= '</tr>';
+ }
+ }
+ if( ($n % 3) != 0 ) {
+ $out .= '</tr>';
+ }
+ $out .= '</table>';
+ } else {
+ $out = '';
+ }
+ }
+
+ if ( $including ) {
+ $out2 = '';
+ } else {
+ $nsForm = $this->namespaceForm ( $namespace, $prefix );
+ $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 ),
+ wfMsg ( 'allpages' ) );
+ if ( isset($dbr) && $dbr && ($n == $this->maxPerPage) && ($s = $dbr->fetchObject( $res )) ) {
+ $namespaceparam = $namespace ? "&namespace=$namespace" : "";
+ $out2 .= " | " . $sk->makeKnownLink(
+ $wgContLang->specialPage( $this->name ),
+ wfMsgHtml( 'nextpage', htmlspecialchars( $s->page_title ) ),
+ "from=" . wfUrlEncode ( $s->page_title ) .
+ "&prefix=" . wfUrlEncode ( $prefix ) . $namespaceparam );
+ }
+ $out2 .= "</td></tr></table><hr />";
+ }
+
+ $wgOut->addHtml( $out2 . $out );
+ }
+}
diff --git a/includes/specials/SpecialProtectedpages.php b/includes/specials/SpecialProtectedpages.php
new file mode 100644
index 00000000..3025c055
--- /dev/null
+++ b/includes/specials/SpecialProtectedpages.php
@@ -0,0 +1,309 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * @todo document
+ * @ingroup SpecialPage
+ */
+class ProtectedPagesForm {
+
+ protected $IdLevel = 'level';
+ protected $IdType = 'type';
+
+ public function showList( $msg = '' ) {
+ global $wgOut, $wgRequest;
+
+ $wgOut->setPagetitle( wfMsg( "protectedpages" ) );
+ if ( "" != $msg ) {
+ $wgOut->setSubtitle( $msg );
+ }
+
+ // Purge expired entries on one in every 10 queries
+ if ( !mt_rand( 0, 10 ) ) {
+ Title::purgeExpiredRestrictions();
+ }
+
+ $type = $wgRequest->getVal( $this->IdType );
+ $level = $wgRequest->getVal( $this->IdLevel );
+ $sizetype = $wgRequest->getVal( 'sizetype' );
+ $size = $wgRequest->getIntOrNull( 'size' );
+ $NS = $wgRequest->getIntOrNull( 'namespace' );
+ $indefOnly = $wgRequest->getBool( 'indefonly' ) ? 1 : 0;
+
+ $pager = new ProtectedPagesPager( $this, array(), $type, $level, $NS, $sizetype, $size, $indefOnly );
+
+ $wgOut->addHTML( $this->showOptions( $NS, $type, $level, $sizetype, $size, $indefOnly ) );
+
+ if ( $pager->getNumRows() ) {
+ $s = $pager->getNavigationBar();
+ $s .= "<ul>" .
+ $pager->getBody() .
+ "</ul>";
+ $s .= $pager->getNavigationBar();
+ } else {
+ $s = '<p>' . wfMsgHtml( 'protectedpagesempty' ) . '</p>';
+ }
+ $wgOut->addHTML( $s );
+ }
+
+ /**
+ * Callback function to output a restriction
+ * @param $row object Protected title
+ * @return string Formatted <li> element
+ */
+ public function formatRow( $row ) {
+ global $wgUser, $wgLang, $wgContLang;
+
+ wfProfileIn( __METHOD__ );
+
+ static $skin=null;
+
+ if( is_null( $skin ) )
+ $skin = $wgUser->getSkin();
+
+ $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title );
+ $link = $skin->makeLinkObj( $title );
+
+ $description_items = array ();
+
+ $protType = wfMsgHtml( 'restriction-level-' . $row->pr_level );
+
+ $description_items[] = $protType;
+
+ if ( $row->pr_cascade ) {
+ $description_items[] = wfMsg( 'protect-summary-cascade' );
+ }
+
+ $expiry_description = '';
+ $stxt = '';
+
+ if ( $row->pr_expiry != 'infinity' && strlen($row->pr_expiry) ) {
+ $expiry = Block::decodeExpiry( $row->pr_expiry );
+
+ $expiry_description = wfMsgForContent( 'protect-expiring', $wgLang->timeanddate( $expiry ) );
+
+ $description_items[] = $expiry_description;
+ }
+
+ if (!is_null($size = $row->page_len)) {
+ $stxt = $wgContLang->getDirMark() . ' ' . $skin->formatRevisionSize( $size );
+ }
+
+ # Show a link to the change protection form for allowed users otherwise a link to the protection log
+ if( $wgUser->isAllowed( 'protect' ) ) {
+ $changeProtection = ' (' . $skin->makeKnownLinkObj( $title, wfMsgHtml( 'protect_change' ), 'action=unprotect' ) . ')';
+ } else {
+ $ltitle = SpecialPage::getTitleFor( 'Log' );
+ $changeProtection = ' (' . $skin->makeKnownLinkObj( $ltitle, wfMsgHtml( 'protectlogpage' ), 'type=protect&page=' . $title->getPrefixedUrl() ) . ')';
+ }
+
+ wfProfileOut( __METHOD__ );
+
+ return '<li>' . wfSpecialList( $link . $stxt, implode( $description_items, ', ' ) ) . $changeProtection . "</li>\n";
+ }
+
+ /**
+ * @param $namespace int
+ * @param $type string
+ * @param $level string
+ * @param $minsize int
+ * @param $indefOnly bool
+ * @return string Input form
+ * @private
+ */
+ protected function showOptions( $namespace, $type='edit', $level, $sizetype, $size, $indefOnly ) {
+ 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" .
+ $this->getNamespaceMenu( $namespace ) . "&nbsp;\n" .
+ $this->getTypeMenu( $type ) . "&nbsp;\n" .
+ $this->getLevelMenu( $level ) . "&nbsp;\n" .
+ "<br /><span style='white-space: nowrap'>&nbsp;&nbsp;" .
+ $this->getExpiryCheck( $indefOnly ) . "&nbsp;\n" .
+ $this->getSizeLimit( $sizetype, $size ) . "&nbsp;\n" .
+ "</span>" .
+ "&nbsp;" . Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" .
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' );
+ }
+
+ /**
+ * Prepare the namespace filter drop-down; standard namespace
+ * selector, sans the MediaWiki namespace
+ *
+ * @param mixed $namespace Pre-select namespace
+ * @return string
+ */
+ protected function getNamespaceMenu( $namespace = null ) {
+ return "<span style='white-space: nowrap'>" .
+ Xml::label( wfMsg( 'namespace' ), 'namespace' ) . '&nbsp;'
+ . Xml::namespaceSelector( $namespace, '' ) . "</span>";
+ }
+
+ /**
+ * @return string Formatted HTML
+ */
+ protected function getExpiryCheck( $indefOnly ) {
+ return
+ Xml::checkLabel( wfMsg('protectedpages-indef'), 'indefonly', 'indefonly', $indefOnly ) . "\n";
+ }
+
+ /**
+ * @return string Formatted HTML
+ */
+ protected function getSizeLimit( $sizetype, $size ) {
+ $max = $sizetype === 'max';
+
+ return
+ Xml::radioLabel( wfMsg('minimum-size'), 'sizetype', 'min', 'wpmin', !$max ) .
+ '&nbsp;' .
+ Xml::radioLabel( wfMsg('maximum-size'), 'sizetype', 'max', 'wpmax', $max ) .
+ '&nbsp;' .
+ Xml::input( 'size', 9, $size, array( 'id' => 'wpsize' ) ) .
+ '&nbsp;' .
+ Xml::label( wfMsg('pagesize'), 'wpsize' );
+ }
+
+ /**
+ * @return string Formatted HTML
+ */
+ protected function getTypeMenu( $pr_type ) {
+ global $wgRestrictionTypes;
+
+ $m = array(); // Temporary array
+ $options = array();
+
+ // First pass to load the log names
+ foreach( $wgRestrictionTypes as $type ) {
+ $text = wfMsg("restriction-$type");
+ $m[$text] = $type;
+ }
+
+ // Third pass generates sorted XHTML content
+ foreach( $m as $text => $type ) {
+ $selected = ($type == $pr_type );
+ $options[] = Xml::option( $text, $type, $selected ) . "\n";
+ }
+
+ return "<span style='white-space: nowrap'>" .
+ Xml::label( wfMsg('restriction-type') , $this->IdType ) . '&nbsp;' .
+ Xml::tags( 'select',
+ array( 'id' => $this->IdType, 'name' => $this->IdType ),
+ implode( "\n", $options ) ) . "</span>";
+ }
+
+ /**
+ * @return string Formatted HTML
+ */
+ protected function getLevelMenu( $pr_level ) {
+ global $wgRestrictionLevels;
+
+ $m = array( wfMsg('restriction-level-all') => 0 ); // Temporary array
+ $options = array();
+
+ // First pass to load the log names
+ foreach( $wgRestrictionLevels as $type ) {
+ if ( $type !='' && $type !='*') {
+ $text = wfMsg("restriction-level-$type");
+ $m[$text] = $type;
+ }
+ }
+
+ // Third pass generates sorted XHTML content
+ foreach( $m as $text => $type ) {
+ $selected = ($type == $pr_level );
+ $options[] = Xml::option( $text, $type, $selected );
+ }
+
+ return
+ Xml::label( wfMsg('restriction-level') , $this->IdLevel ) . '&nbsp;' .
+ Xml::tags( 'select',
+ array( 'id' => $this->IdLevel, 'name' => $this->IdLevel ),
+ implode( "\n", $options ) );
+ }
+}
+
+/**
+ * @todo document
+ * @ingroup Pager
+ */
+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 ) {
+ $this->mForm = $form;
+ $this->mConds = $conds;
+ $this->type = ( $type ) ? $type : 'edit';
+ $this->level = $level;
+ $this->namespace = $namespace;
+ $this->sizetype = $sizetype;
+ $this->size = intval($size);
+ $this->indefonly = (bool)$indefonly;
+ parent::__construct();
+ }
+
+ function getStartBody() {
+ wfProfileIn( __METHOD__ );
+ # Do a link batch query
+ $lb = new LinkBatch;
+ while( $row = $this->mResult->fetchObject() ) {
+ $lb->add( $row->page_namespace, $row->page_title );
+ }
+ $lb->execute();
+
+ wfProfileOut( __METHOD__ );
+ return '';
+ }
+
+ function formatRow( $row ) {
+ return $this->mForm->formatRow( $row );
+ }
+
+ function getQueryInfo() {
+ $conds = $this->mConds;
+ $conds[] = 'pr_expiry>' . $this->mDb->addQuotes( $this->mDb->timestamp() );
+ $conds[] = 'page_id=pr_page';
+ $conds[] = 'pr_type=' . $this->mDb->addQuotes( $this->type );
+
+ if( $this->sizetype=='min' ) {
+ $conds[] = 'page_len>=' . $this->size;
+ } else if( $this->sizetype=='max' ) {
+ $conds[] = 'page_len<=' . $this->size;
+ }
+
+ if( $this->indefonly ) {
+ $conds[] = "pr_expiry = 'infinity' OR pr_expiry IS NULL";
+ }
+
+ if( $this->level )
+ $conds[] = 'pr_level=' . $this->mDb->addQuotes( $this->level );
+ if( !is_null($this->namespace) )
+ $conds[] = 'page_namespace=' . $this->mDb->addQuotes( $this->namespace );
+ return array(
+ 'tables' => array( 'page_restrictions', 'page' ),
+ 'fields' => 'pr_id,page_namespace,page_title,page_len,pr_type,pr_level,pr_expiry,pr_cascade',
+ 'conds' => $conds
+ );
+ }
+
+ function getIndexField() {
+ return 'pr_id';
+ }
+}
+
+/**
+ * Constructor
+ */
+function wfSpecialProtectedpages() {
+
+ $ppForm = new ProtectedPagesForm();
+
+ $ppForm->showList();
+}
diff --git a/includes/specials/SpecialProtectedtitles.php b/includes/specials/SpecialProtectedtitles.php
new file mode 100644
index 00000000..2ec68a66
--- /dev/null
+++ b/includes/specials/SpecialProtectedtitles.php
@@ -0,0 +1,216 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * @todo document
+ * @ingroup SpecialPage
+ */
+class ProtectedTitlesForm {
+
+ protected $IdLevel = 'level';
+ protected $IdType = 'type';
+
+ function showList( $msg = '' ) {
+ global $wgOut, $wgRequest;
+
+ $wgOut->setPagetitle( wfMsg( "protectedtitles" ) );
+ if ( "" != $msg ) {
+ $wgOut->setSubtitle( $msg );
+ }
+
+ // Purge expired entries on one in every 10 queries
+ if ( !mt_rand( 0, 10 ) ) {
+ Title::purgeExpiredRestrictions();
+ }
+
+ $type = $wgRequest->getVal( $this->IdType );
+ $level = $wgRequest->getVal( $this->IdLevel );
+ $sizetype = $wgRequest->getVal( 'sizetype' );
+ $size = $wgRequest->getIntOrNull( 'size' );
+ $NS = $wgRequest->getIntOrNull( 'namespace' );
+
+ $pager = new ProtectedTitlesPager( $this, array(), $type, $level, $NS, $sizetype, $size );
+
+ $wgOut->addHTML( $this->showOptions( $NS, $type, $level, $sizetype, $size ) );
+
+ if ( $pager->getNumRows() ) {
+ $s = $pager->getNavigationBar();
+ $s .= "<ul>" .
+ $pager->getBody() .
+ "</ul>";
+ $s .= $pager->getNavigationBar();
+ } else {
+ $s = '<p>' . wfMsgHtml( 'protectedtitlesempty' ) . '</p>';
+ }
+ $wgOut->addHTML( $s );
+ }
+
+ /**
+ * Callback function to output a restriction
+ */
+ function formatRow( $row ) {
+ global $wgUser, $wgLang, $wgContLang;
+
+ wfProfileIn( __METHOD__ );
+
+ static $skin=null;
+
+ if( is_null( $skin ) )
+ $skin = $wgUser->getSkin();
+
+ $title = Title::makeTitleSafe( $row->pt_namespace, $row->pt_title );
+ $link = $skin->makeLinkObj( $title );
+
+ $description_items = array ();
+
+ $protType = wfMsgHtml( 'restriction-level-' . $row->pt_create_perm );
+
+ $description_items[] = $protType;
+
+ $expiry_description = ''; $stxt = '';
+
+ if ( $row->pt_expiry != 'infinity' && strlen($row->pt_expiry) ) {
+ $expiry = Block::decodeExpiry( $row->pt_expiry );
+
+ $expiry_description = wfMsgForContent( 'protect-expiring', $wgLang->timeanddate( $expiry ) );
+
+ $description_items[] = $expiry_description;
+ }
+
+ wfProfileOut( __METHOD__ );
+
+ return '<li>' . wfSpecialList( $link . $stxt, implode( $description_items, ', ' ) ) . "</li>\n";
+ }
+
+ /**
+ * @param $namespace int
+ * @param $type string
+ * @param $level string
+ * @param $minsize int
+ * @private
+ */
+ function showOptions( $namespace, $type='edit', $level, $sizetype, $size ) {
+ global $wgScript;
+ $action = htmlspecialchars( $wgScript );
+ $title = SpecialPage::getTitleFor( 'ProtectedTitles' );
+ $special = htmlspecialchars( $title->getPrefixedDBkey() );
+ return "<form action=\"$action\" method=\"get\">\n" .
+ '<fieldset>' .
+ Xml::element( 'legend', array(), wfMsg( 'protectedtitles' ) ) .
+ Xml::hidden( 'title', $special ) . "&nbsp;\n" .
+ $this->getNamespaceMenu( $namespace ) . "&nbsp;\n" .
+ // $this->getLevelMenu( $level ) . "<br/>\n" .
+ "&nbsp;" . Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" .
+ "</fieldset></form>";
+ }
+
+ /**
+ * Prepare the namespace filter drop-down; standard namespace
+ * selector, sans the MediaWiki namespace
+ *
+ * @param mixed $namespace Pre-select namespace
+ * @return string
+ */
+ function getNamespaceMenu( $namespace = null ) {
+ return Xml::label( wfMsg( 'namespace' ), 'namespace' )
+ . '&nbsp;'
+ . Xml::namespaceSelector( $namespace, '' );
+ }
+
+ /**
+ * @return string Formatted HTML
+ * @private
+ */
+ function getLevelMenu( $pr_level ) {
+ global $wgRestrictionLevels;
+
+ $m = array( wfMsg('restriction-level-all') => 0 ); // Temporary array
+ $options = array();
+
+ // First pass to load the log names
+ foreach( $wgRestrictionLevels as $type ) {
+ if ( $type !='' && $type !='*') {
+ $text = wfMsg("restriction-level-$type");
+ $m[$text] = $type;
+ }
+ }
+
+ // Third pass generates sorted XHTML content
+ foreach( $m as $text => $type ) {
+ $selected = ($type == $pr_level );
+ $options[] = Xml::option( $text, $type, $selected );
+ }
+
+ return
+ Xml::label( wfMsg('restriction-level') , $this->IdLevel ) . '&nbsp;' .
+ Xml::tags( 'select',
+ array( 'id' => $this->IdLevel, 'name' => $this->IdLevel ),
+ implode( "\n", $options ) );
+ }
+}
+
+/**
+ * @todo document
+ * @ingroup Pager
+ */
+class ProtectedtitlesPager extends AlphabeticPager {
+ public $mForm, $mConds;
+
+ function __construct( $form, $conds = array(), $type, $level, $namespace, $sizetype='', $size=0 ) {
+ $this->mForm = $form;
+ $this->mConds = $conds;
+ $this->level = $level;
+ $this->namespace = $namespace;
+ $this->size = intval($size);
+ parent::__construct();
+ }
+
+ function getStartBody() {
+ wfProfileIn( __METHOD__ );
+ # Do a link batch query
+ $this->mResult->seek( 0 );
+ $lb = new LinkBatch;
+
+ while ( $row = $this->mResult->fetchObject() ) {
+ $lb->add( $row->pt_namespace, $row->pt_title );
+ }
+
+ $lb->execute();
+ wfProfileOut( __METHOD__ );
+ return '';
+ }
+
+ function formatRow( $row ) {
+ return $this->mForm->formatRow( $row );
+ }
+
+ function getQueryInfo() {
+ $conds = $this->mConds;
+ $conds[] = 'pt_expiry>' . $this->mDb->addQuotes( $this->mDb->timestamp() );
+
+ if( !is_null($this->namespace) )
+ $conds[] = 'pt_namespace=' . $this->mDb->addQuotes( $this->namespace );
+ return array(
+ 'tables' => 'protected_titles',
+ 'fields' => 'pt_namespace,pt_title,pt_create_perm,pt_expiry,pt_timestamp',
+ 'conds' => $conds
+ );
+ }
+
+ function getIndexField() {
+ return 'pt_timestamp';
+ }
+}
+
+/**
+ * Constructor
+ */
+function wfSpecialProtectedtitles() {
+
+ $ppForm = new ProtectedTitlesForm();
+
+ $ppForm->showList();
+}
diff --git a/includes/specials/SpecialRandompage.php b/includes/specials/SpecialRandompage.php
new file mode 100644
index 00000000..0e7ada1d
--- /dev/null
+++ b/includes/specials/SpecialRandompage.php
@@ -0,0 +1,100 @@
+<?php
+
+/**
+ * Special page to direct the user to a random page
+ *
+ * @ingroup SpecialPage
+ * @author Rob Church <robchur@gmail.com>, Ilmari Karonen
+ * @license GNU General Public Licence 2.0 or later
+ */
+class RandomPage extends SpecialPage {
+ private $namespace = NS_MAIN; // namespace to select pages from
+
+ function __construct( $name = 'Randompage' ){
+ parent::__construct( $name );
+ }
+
+ public function getNamespace() {
+ return $this->namespace;
+ }
+
+ public function setNamespace ( $ns ) {
+ if( $ns < NS_MAIN ) $ns = NS_MAIN;
+ $this->namespace = $ns;
+ }
+
+ // select redirects instead of normal pages?
+ // Overriden by SpecialRandomredirect
+ public function isRedirect(){
+ return false;
+ }
+
+ public function execute( $par ) {
+ global $wgOut, $wgContLang;
+
+ if ($par)
+ $this->setNamespace( $wgContLang->getNsIndex( $par ) );
+
+ $title = $this->getRandomTitle();
+
+ if( is_null( $title ) ) {
+ $this->setHeaders();
+ $wgOut->addWikiMsg( strtolower( $this->mName ) . '-nopages' );
+ return;
+ }
+
+ $query = $this->isRedirect() ? 'redirect=no' : '';
+ $wgOut->redirect( $title->getFullUrl( $query ) );
+ }
+
+
+ /**
+ * Choose a random title.
+ * @return Title object (or null if nothing to choose from)
+ */
+ public function getRandomTitle() {
+ $randstr = wfRandom();
+ $row = $this->selectRandomPageFromDB( $randstr );
+
+ /* If we picked a value that was higher than any in
+ * the DB, wrap around and select the page with the
+ * lowest value instead! One might think this would
+ * skew the distribution, but in fact it won't cause
+ * any more bias than what the page_random scheme
+ * causes anyway. Trust me, I'm a mathematician. :)
+ */
+ if( !$row )
+ $row = $this->selectRandomPageFromDB( "0" );
+
+ if( $row )
+ return Title::makeTitleSafe( $this->namespace, $row->page_title );
+ else
+ return null;
+ }
+
+ private function selectRandomPageFromDB( $randstr ) {
+ global $wgExtraRandompageSQL;
+ $fname = 'RandomPage::selectRandomPageFromDB';
+
+ $dbr = wfGetDB( DB_SLAVE );
+
+ $use_index = $dbr->useIndexClause( 'page_random' );
+ $page = $dbr->tableName( 'page' );
+
+ $ns = (int) $this->namespace;
+ $redirect = $this->isRedirect() ? 1 : 0;
+
+ $extra = $wgExtraRandompageSQL ? "AND ($wgExtraRandompageSQL)" : "";
+ $sql = "SELECT page_title
+ FROM $page $use_index
+ WHERE page_namespace = $ns
+ AND page_is_redirect = $redirect
+ AND page_random >= $randstr
+ $extra
+ ORDER BY page_random";
+
+ $sql = $dbr->limitResult( $sql, 1, 0 );
+ $res = $dbr->query( $sql, $fname );
+ return $dbr->fetchObject( $res );
+ }
+}
diff --git a/includes/specials/SpecialRandomredirect.php b/includes/specials/SpecialRandomredirect.php
new file mode 100644
index 00000000..629d5b3c
--- /dev/null
+++ b/includes/specials/SpecialRandomredirect.php
@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * Special page to direct the user to a random redirect page (minus the second redirect)
+ *
+ * @ingroup SpecialPage
+ * @author Rob Church <robchur@gmail.com>, Ilmari Karonen
+ * @license GNU General Public Licence 2.0 or later
+ */
+class SpecialRandomredirect extends RandomPage {
+ function __construct(){
+ parent::__construct( 'Randomredirect' );
+ }
+
+ // Override parent::isRedirect()
+ public function isRedirect(){
+ return true;
+ }
+}
diff --git a/includes/specials/SpecialRecentchanges.php b/includes/specials/SpecialRecentchanges.php
new file mode 100644
index 00000000..cb718bdc
--- /dev/null
+++ b/includes/specials/SpecialRecentchanges.php
@@ -0,0 +1,662 @@
+<?php
+
+/**
+ * Implements Special:Recentchanges
+ * @ingroup SpecialPage
+ */
+class SpecialRecentChanges extends SpecialPage {
+ public function __construct() {
+ SpecialPage::SpecialPage( 'Recentchanges' );
+ $this->includable( true );
+ }
+
+ /**
+ * Get a FormOptions object containing the default options
+ *
+ * @return FormOptions
+ */
+ public function getDefaultOptions() {
+ $opts = new FormOptions();
+
+ $opts->add( 'days', (int)User::getDefaultOption( 'rcdays' ) );
+ $opts->add( 'limit', (int)User::getDefaultOption( 'rclimit' ) );
+ $opts->add( 'from', '' );
+
+ $opts->add( 'hideminor', false );
+ $opts->add( 'hidebots', true );
+ $opts->add( 'hideanons', false );
+ $opts->add( 'hideliu', false );
+ $opts->add( 'hidepatrolled', false );
+ $opts->add( 'hidemyself', false );
+
+ $opts->add( 'namespace', '', FormOptions::INTNULL );
+ $opts->add( 'invert', false );
+
+ $opts->add( 'categories', '' );
+ $opts->add( 'categories_any', false );
+
+ return $opts;
+ }
+
+ /**
+ * Get a FormOptions object with options as specified by the user
+ *
+ * @return FormOptions
+ */
+ public function setup( $parameters ) {
+ global $wgUser, $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 ) {
+ $this->parseParameters( $parameters, $opts );
+ }
+
+ $opts->validateIntBounds( 'limit', 0, 5000 );
+ return $opts;
+ }
+
+ /**
+ * Get a FormOptions object sepcific for feed requests
+ *
+ * @return FormOptions
+ */
+ public function feedSetup() {
+ global $wgFeedLimit, $wgRequest;
+ $opts = $this->getDefaultOptions();
+ $opts->fetchValuesFromRequest( $wgRequest, array( 'days', 'limit', 'hideminor' ) );
+ $opts->validateIntBounds( 'limit', 0, $wgFeedLimit );
+ return $opts;
+ }
+
+ /**
+ * Main execution point
+ *
+ * @param $parameters string
+ */
+ public function execute( $parameters ) {
+ global $wgRequest, $wgOut;
+ $feedFormat = $wgRequest->getVal( 'feed' );
+
+ # 10 seconds server-side caching max
+ $wgOut->setSquidMaxage( 10 );
+
+ $lastmod = $this->checkLastModified( $feedFormat );
+ if( $lastmod === false ){
+ return;
+ }
+
+ $opts = $feedFormat ? $this->feedSetup() : $this->setup( $parameters );
+ $this->setHeaders();
+ $this->outputHeader();
+
+ // 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 );
+ return;
+ }
+ $dbr = wfGetDB( DB_SLAVE );
+ while( $row = $dbr->fetchObject( $res ) ){
+ $rows[] = $row;
+ if ( !$feedFormat ) {
+ // User page and talk links
+ $batch->add( NS_USER, $row->rc_user_text );
+ $batch->add( NS_USER_TALK, $row->rc_user_text );
+ }
+
+ }
+ $dbr->freeResult( $res );
+
+ if ( $feedFormat ) {
+ list( $feed, $feedObj ) = $this->getFeedObject( $feedFormat );
+ $feed->execute( $feedObj, $rows, $opts['limit'], $opts['hideminor'], $lastmod );
+ } else {
+ $batch->execute();
+ $this->webOutput( $rows, $opts );
+ }
+
+ }
+
+ /**
+ * Return an array with a ChangesFeed object and ChannelFeed object
+ *
+ * @return array
+ */
+ public function getFeedObject( $feedFormat ){
+ $feed = new ChangesFeed( $feedFormat, 'rcfeed' );
+ $feedObj = $feed->getFeedObject(
+ wfMsgForContent( 'recentchanges' ),
+ wfMsgForContent( 'recentchanges-feed-description' )
+ );
+ return array( $feed, $feedObj );
+ }
+
+ /**
+ * Process $par and put options found if $opts
+ * Mainly used when including the page
+ *
+ * @param $par String
+ * @param $opts FormOptions
+ */
+ 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;
+
+ $m = array();
+ if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) ) $opts['limit'] = $m[1];
+ if ( preg_match( '/^days=(\d+)$/', $bit, $m ) ) $opts['days'] = $m[1];
+ }
+ }
+
+ /**
+ * Get last modified date, for client caching
+ * Don't use this if we are using the patrol feature, patrol changes don't
+ * update the timestamp
+ *
+ * @param $feedFormat String
+ * @return int 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 ) ){
+ # Client cache fresh and headers sent, nothing more to do.
+ return false;
+ }
+ }
+ return $lastmod;
+ }
+
+ /**
+ * Return an array of conditions depending of options set in $opts
+ *
+ * @param $opts FormOptions
+ * @return array
+ */
+ public function buildMainQueryConds( FormOptions $opts ) {
+ global $wgUser;
+
+ $dbr = wfGetDB( DB_SLAVE );
+ $conds = array();
+
+ # It makes no sense to hide both anons and logged-in users
+ # Where this occurs, force anons to be shown
+ $forcebot = false;
+ if( $opts['hideanons'] && $opts['hideliu'] ){
+ # Check if the user wants to show bots only
+ if( $opts['hidebots'] ){
+ $opts['hideanons'] = false;
+ } else {
+ $forcebot = true;
+ $opts['hidebots'] = false;
+ }
+ }
+
+ // Calculate cutoff
+ $cutoff_unixtime = time() - ( $opts['days'] * 86400 );
+ $cutoff_unixtime = $cutoff_unixtime - ($cutoff_unixtime % 86400);
+ $cutoff = $dbr->timestamp( $cutoff_unixtime );
+
+ $fromValid = preg_match('/^[0-9]{14}$/', $opts['from']);
+ if( $fromValid && $opts['from'] > wfTimestamp(TS_MW,$cutoff) ) {
+ $cutoff = $dbr->timestamp($opts['from']);
+ } else {
+ $opts->reset( 'from' );
+ }
+
+ $conds[] = 'rc_timestamp >= ' . $dbr->addQuotes( $cutoff );
+
+
+ $hidePatrol = $wgUser->useRCPatrol() && $opts['hidepatrolled'];
+ $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['hidemyself'] ) {
+ if( $wgUser->getId() ) {
+ $conds[] = 'rc_user != ' . $dbr->addQuotes( $wgUser->getId() );
+ } else {
+ $conds[] = 'rc_user_text != ' . $dbr->addQuotes( $wgUser->getName() );
+ }
+ }
+
+ # Namespace filtering
+ if ( $opts['namespace'] !== '' ) {
+ if ( !$opts['invert'] ) {
+ $conds[] = 'rc_namespace = ' . $dbr->addQuotes( $opts['namespace'] );
+ } else {
+ $conds[] = 'rc_namespace != ' . $dbr->addQuotes( $opts['namespace'] );
+ }
+ }
+
+ return $conds;
+ }
+
+ /**
+ * Process the query
+ *
+ * @param $conds array
+ * @param $opts FormOptions
+ * @return database result or false (for Recentchangeslinked only)
+ */
+ public function doMainQuery( $conds, $opts ) {
+ global $wgUser;
+
+ $tables = array( 'recentchanges' );
+ $join_conds = array();
+
+ $uid = $wgUser->getId();
+ $dbr = wfGetDB( DB_SLAVE );
+ $limit = $opts['limit'];
+ $namespace = $opts['namespace'];
+ $invert = $opts['invert'];
+
+ // 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") );
+ }
+
+ wfRunHooks('SpecialRecentChangesQuery', array( &$conds, &$tables, &$join_conds, $opts ) );
+
+ // Is there either one namespace selected or excluded?
+ // Also, if this is "all" or main namespace, just use timestamp index.
+ if( is_null($namespace) || $invert || $namespace == NS_MAIN ) {
+ $res = $dbr->select( $tables, '*', $conds, __METHOD__,
+ array( 'ORDER BY' => 'rc_timestamp DESC', 'LIMIT' => $limit,
+ 'USE INDEX' => array('recentchanges' => 'rc_timestamp') ),
+ $join_conds );
+ // We have a new_namespace_time index! UNION over new=(0,1) and sort result set!
+ } else {
+ // New pages
+ $sqlNew = $dbr->selectSQLText( $tables, '*',
+ array( 'rc_new' => 1 ) + $conds,
+ __METHOD__,
+ array( 'ORDER BY' => 'rc_timestamp DESC', 'LIMIT' => $limit,
+ 'USE INDEX' => array('recentchanges' => 'new_name_timestamp') ),
+ $join_conds );
+ // Old pages
+ $sqlOld = $dbr->selectSQLText( $tables, '*',
+ array( 'rc_new' => 0 ) + $conds,
+ __METHOD__,
+ array( 'ORDER BY' => 'rc_timestamp DESC', 'LIMIT' => $limit,
+ 'USE INDEX' => array('recentchanges' => 'new_name_timestamp') ),
+ $join_conds );
+ # Join the two fast queries, and sort the result set
+ $sql = "($sqlNew) UNION ($sqlOld) ORDER BY rc_timestamp DESC LIMIT $limit";
+ $res = $dbr->query( $sql, __METHOD__ );
+ }
+
+ return $res;
+ }
+
+ /**
+ * Send output to $wgOut, only called if not used feeds
+ *
+ * @param $rows array of database rows
+ * @param $opts FormOptions
+ */
+ public function webOutput( $rows, $opts ) {
+ global $wgOut, $wgUser, $wgRCShowWatchingUsers, $wgShowUpdatedMarker;
+ global $wgAllowCategorizedRecentChanges;
+
+ $limit = $opts['limit'];
+
+ if ( !$this->including() ) {
+ // Output options box
+ $this->doHeader( $opts );
+ }
+
+ // And now for the content
+ $wgOut->setSyndicated( true );
+
+ $list = ChangesList::newFromUser( $wgUser );
+
+ 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;
+ }
+
+ $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->recentChangesLine( $rc, !empty( $obj->wl_user ) );
+ --$limit;
+ }
+ }
+ $s .= $list->endRecentChangesList();
+ $wgOut->addHTML( $s );
+ }
+
+ /**
+ * Return the text to be displayed above the changes
+ *
+ * @param $opts FormOptions
+ * @return String: XHTML
+ */
+ public function doHeader( $opts ) {
+ global $wgScript, $wgOut;
+
+ $this->setTopText( $wgOut, $opts );
+
+ $defaults = $opts->getAllValues();
+ $nondefaults = $opts->getChangedValues();
+ $opts->consumeValues( array( 'namespace', 'invert' ) );
+
+ $panel = array();
+ $panel[] = $this->optionsPanel( $defaults, $nondefaults );
+ $panel[] = '<hr />';
+
+ $extraOpts = $this->getExtraOptions( $opts );
+
+ $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] );
+ } else {
+ $out .= Xml::tags( 'td', array( 'colspan' => 2 ), $optionRow );
+ }
+ $out .= Xml::closeElement( 'tr' );
+ }
+ $out .= Xml::closeElement( 'table' );
+
+ $unconsumed = $opts->getUnconsumedValues();
+ foreach ( $unconsumed as $key => $value ) {
+ $out .= Xml::hidden( $key, $value );
+ }
+
+ $t = $this->getTitle();
+ $out .= Xml::hidden( 'title', $t->getPrefixedText() );
+ $form = Xml::tags( 'form', array( 'action' => $wgScript ), $out );
+ $panel[] = $form;
+ $panelString = implode( "\n", $panel );
+
+ $wgOut->addHTML(
+ Xml::fieldset( wfMsg( strtolower( $this->mName ) ), $panelString, array( 'class' => 'rcoptions' ) )
+ );
+
+ $this->setBottomText( $wgOut, $opts );
+ }
+
+ /**
+ * Get options to be displayed in a form
+ *
+ * @param $opts FormOptions
+ * @return array
+ */
+ function getExtraOptions( $opts ){
+ $extraOpts = array();
+ $extraOpts['namespace'] = $this->namespaceFilterForm( $opts );
+
+ global $wgAllowCategorizedRecentChanges;
+ if ( $wgAllowCategorizedRecentChanges ) {
+ $extraOpts['category'] = $this->categoryFilterForm( $opts );
+ }
+
+ wfRunHooks( 'SpecialRecentChangesPanel', array( &$extraOpts, $opts ) );
+ $extraOpts['submit'] = Xml::submitbutton( wfMsg('allpagessubmit') );
+ return $extraOpts;
+ }
+
+ /**
+ * Send the text to be displayed above the options
+ *
+ * @param $out OutputPage
+ * @param $opts FormOptions
+ */
+ function setTopText( &$out, $opts ){
+ $out->addWikiText( wfMsgForContentNoTrans( 'recentchangestext' ) );
+ }
+
+ /**
+ * Send the text to be displayed after the options, for use in
+ * Recentchangeslinked
+ *
+ * @param $out OutputPage
+ * @param $opts FormOptions
+ */
+ function setBottomText( &$out, $opts ){}
+
+ /**
+ * Creates the choose namespace selection
+ *
+ * @param $opts FormOptions
+ * @return string
+ */
+ protected function namespaceFilterForm( FormOptions $opts ) {
+ $nsSelect = HTMLnamespaceselector( $opts['namespace'], '' );
+ $nsLabel = Xml::label( wfMsg('namespace'), 'namespace' );
+ $invert = Xml::checkLabel( wfMsg('invert'), 'invert', 'nsinvert', $opts['invert'] );
+ return array( $nsLabel, "$nsSelect $invert" );
+ }
+
+ /**
+ * Create a input to filter changes by categories
+ *
+ * @param $opts FormOptions
+ * @return array
+ */
+ protected function categoryFilterForm( FormOptions $opts ) {
+ list( $label, $input ) = Xml::inputLabelSep( wfMsg('rc_categories'),
+ 'categories', 'mw-categories', false, $opts['categories'] );
+
+ $input .= ' ' . Xml::checkLabel( wfMsg('rc_categories_any'),
+ 'categories_any', 'mw-categories_any', $opts['categories_any'] );
+
+ return array( $label, $input );
+ }
+
+ /**
+ * Filter $rows by categories set in $opts
+ *
+ * @param $rows array of database rows
+ * @param $opts FormOptions
+ */
+ function filterByCategories( &$rows, FormOptions $opts ) {
+ $categories = array_map( 'trim', explode( "|" , $opts['categories'] ) );
+
+ if( empty($categories) ) {
+ return;
+ }
+
+ # Filter categories
+ $cats = array();
+ foreach ( $categories as $cat ) {
+ $cat = trim( $cat );
+ if ( $cat == "" ) continue;
+ $cats[] = $cat;
+ }
+
+ # Filter articles
+ $articles = array();
+ $a2r = array();
+ 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) ) {
+ $articles[] = $id;
+ }
+ if ( !isset($a2r[$id]) ) {
+ $a2r[$id] = array();
+ }
+ $a2r[$id][] = $k;
+ }
+
+ # Shortcut?
+ if ( !count($articles) || !count($cats) )
+ return ;
+
+ # Look up
+ $c = new Categoryfinder ;
+ $c->seed( $articles, $cats, $opts['categories_any'] ? "OR" : "AND" ) ;
+ $match = $c->run();
+
+ # Filter
+ $newrows = array();
+ foreach ( $match AS $id ) {
+ foreach ( $a2r[$id] AS $rev ) {
+ $k = $rev;
+ $newrows[$k] = $rows[$k];
+ }
+ }
+ $rows = $newrows;
+ }
+
+ /**
+ * 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();
+ return $sk->makeKnownLinkObj( $this->getTitle(), htmlspecialchars( $title ),
+ wfArrayToCGI( $override, $options ), '', '', $active ? 'style="font-weight: bold;"' : '' );
+ }
+
+ /**
+ * Creates the options panel.
+ * @param $defaults array
+ * @param $nondefaults array
+ */
+ function optionsPanel( $defaults, $nondefaults ) {
+ global $wgLang, $wgUser, $wgRCLinkLimits, $wgRCLinkDays;
+
+ $options = $nondefaults + $defaults;
+
+ 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 ) );
+
+ # 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);
+
+ // limit links
+ foreach( $wgRCLinkLimits as $value ) {
+ $cl[] = $this->makeOptionsLink( $wgLang->formatNum( $value ),
+ array( 'limit' => $value ), $nondefaults, $value == $options['limit'] ) ;
+ }
+ $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);
+
+
+ // show/hide links
+ $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']],
+ array( 'hidebots' => 1-$options['hidebots'] ), $nondefaults);
+ $anonsLink = $this->makeOptionsLink( $showhide[ 1 - $options['hideanons'] ],
+ array( 'hideanons' => 1 - $options['hideanons'] ), $nondefaults );
+ $liuLink = $this->makeOptionsLink( $showhide[1-$options['hideliu']],
+ array( 'hideliu' => 1-$options['hideliu'] ), $nondefaults);
+ $patrLink = $this->makeOptionsLink( $showhide[1-$options['hidepatrolled']],
+ array( 'hidepatrolled' => 1-$options['hidepatrolled'] ), $nondefaults);
+ $myselfLink = $this->makeOptionsLink( $showhide[1-$options['hidemyself']],
+ array( 'hidemyself' => 1-$options['hidemyself'] ), $nondefaults);
+
+ $links[] = wfMsgHtml( 'rcshowhideminor', $minorLink );
+ $links[] = wfMsgHtml( 'rcshowhidebots', $botLink );
+ $links[] = wfMsgHtml( 'rcshowhideanons', $anonsLink );
+ $links[] = wfMsgHtml( 'rcshowhideliu', $liuLink );
+ if( $wgUser->useRCPatrol() )
+ $links[] = wfMsgHtml( 'rcshowhidepatr', $patrLink );
+ $links[] = wfMsgHtml( 'rcshowhidemine', $myselfLink );
+ $hl = implode( ' | ', $links );
+
+ // show from this onward link
+ $now = $wgLang->timeanddate( wfTimestampNow(), true );
+ $tl = $this->makeOptionsLink( $now, array( 'from' => wfTimestampNow()), $nondefaults );
+
+ $rclinks = wfMsgExt( 'rclinks', array( 'parseinline', 'replaceafter'),
+ $cl, $dl, $hl );
+ $rclistfrom = wfMsgExt( 'rclistfrom', array( 'parseinline', 'replaceafter'), $tl );
+ return "$note<br />$rclinks<br />$rclistfrom";
+ }
+}
diff --git a/includes/specials/SpecialRecentchangeslinked.php b/includes/specials/SpecialRecentchangeslinked.php
new file mode 100644
index 00000000..d773fb77
--- /dev/null
+++ b/includes/specials/SpecialRecentchangeslinked.php
@@ -0,0 +1,178 @@
+<?php
+
+/**
+ * This is to display changes made to all articles linked in an article.
+ * @ingroup SpecialPage
+ */
+class SpecialRecentchangeslinked extends SpecialRecentchanges {
+
+ function __construct(){
+ SpecialPage::SpecialPage( 'Recentchangeslinked' );
+ }
+
+ public function getDefaultOptions() {
+ $opts = parent::getDefaultOptions();
+ $opts->add( 'target', '' );
+ $opts->add( 'showlinkedto', false );
+ return $opts;
+ }
+
+ public function parseParameters( $par, FormOptions $opts ) {
+ $opts['target'] = $par;
+ }
+
+ public function feedSetup(){
+ global $wgRequest;
+ $opts = parent::feedSetup();
+ $opts['target'] = $wgRequest->getVal( 'target' );
+ return $opts;
+ }
+
+ public function getFeedObject( $feedFormat ){
+ $feed = new ChangesFeed( $feedFormat, false );
+ $feedObj = $feed->getFeedObject(
+ wfMsgForContent( 'recentchangeslinked-title', $this->mTargetTitle->getPrefixedText() ),
+ wfMsgForContent( 'recentchangeslinked' )
+ );
+ return array( $feed, $feedObj );
+ }
+
+ public function doMainQuery( $conds, $opts ) {
+ global $wgUser, $wgOut;
+
+ $target = $opts['target'];
+ $showlinkedto = $opts['showlinkedto'];
+ $limit = $opts['limit'];
+
+ if ( $target === '' ) {
+ return false;
+ }
+ $title = Title::newFromURL( $target );
+ if( !$title || $title->getInterwiki() != '' ){
+ $wgOut->wrapWikiMsg( '<div class="errorbox">$1</div><br clear="both" />', 'allpagesbadtitle' );
+ return false;
+ }
+ $this->mTargetTitle = $title;
+
+ $wgOut->setPageTitle( wfMsg( 'recentchangeslinked-title', $title->getPrefixedText() ) );
+
+ /*
+ * Ordinary links are in the pagelinks table, while transclusions are
+ * in the templatelinks table, categorizations in categorylinks and
+ * image use in imagelinks. We need to somehow combine all these.
+ * Special:Whatlinkshere does this by firing multiple queries and
+ * merging the results, but the code we inherit from our parent class
+ * expects only one result set so we use UNION instead.
+ */
+
+ $dbr = wfGetDB( DB_SLAVE, 'recentchangeslinked' );
+ $id = $title->getArticleId();
+ $ns = $title->getNamespace();
+ $dbkey = $title->getDBkey();
+
+ $tables = array( 'recentchanges' );
+ $select = array( $dbr->tableName( 'recentchanges' ) . '.*' );
+ $join_conds = array();
+
+ // left join with watchlist table to highlight watched rows
+ if( $uid = $wgUser->getId() ) {
+ $tables[] = 'watchlist';
+ $select[] = 'wl_user';
+ $join_conds['watchlist'] = array( 'LEFT JOIN', "wl_user={$uid} AND wl_title=rc_title AND wl_namespace=rc_namespace" );
+ }
+
+ // XXX: parent class does this, should we too?
+ // wfRunHooks('SpecialRecentChangesQuery', array( &$conds, &$tables, &$join_conds, $opts ) );
+
+ if( $ns == NS_CATEGORY && !$showlinkedto ) {
+ // special handling for categories
+ // XXX: should try to make this less klugy
+ $link_tables = array( 'categorylinks' );
+ $showlinkedto = true;
+ } 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';
+ }
+
+ // field name prefixes for all the various tables we might want to join with
+ $prefix = array( 'pagelinks' => 'pl', 'templatelinks' => 'tl', 'categorylinks' => 'cl', 'imagelinks' => 'il' );
+
+ $subsql = array(); // SELECT statements to combine with UNION
+
+ foreach( $link_tables as $link_table ) {
+ $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;
+ else if( $link_table == 'categorylinks' ) $link_ns = NS_CATEGORY;
+ else $link_ns = 0;
+
+ if( $showlinkedto ) {
+ // find changes to pages linking to this page
+ if( $link_ns ) {
+ if( $ns != $link_ns ) continue; // should never happen, but check anyway
+ $subconds = array( "{$pfx}_to" => $dbkey );
+ } else {
+ $subconds = array( "{$pfx}_namespace" => $ns, "{$pfx}_title" => $dbkey );
+ }
+ $subjoin = "rc_cur_id = {$pfx}_from";
+ } else {
+ // find changes to pages linked from this page
+ $subconds = array( "{$pfx}_from" => $id );
+ if( $link_table == 'imagelinks' || $link_table == 'categorylinks' ) {
+ $subconds["rc_namespace"] = $link_ns;
+ $subjoin = "rc_title = {$pfx}_to";
+ } else {
+ $subjoin = "rc_namespace = {$pfx}_namespace AND rc_title = {$pfx}_title";
+ }
+ }
+
+ $subsql[] = $dbr->selectSQLText( array_merge( $tables, array( $link_table ) ), $select, $conds + $subconds,
+ __METHOD__, array( 'ORDER BY' => 'rc_timestamp DESC', 'LIMIT' => $limit ),
+ $join_conds + array( $link_table => array( 'INNER JOIN', $subjoin ) ) );
+ }
+
+ if( count($subsql) == 0 )
+ return false; // should never happen
+ if( count($subsql) == 1 )
+ $sql = $subsql[0];
+ else {
+ // need to resort and relimit after union
+ $sql = "(" . implode( ") UNION (", $subsql ) . ") ORDER BY rc_timestamp DESC LIMIT {$limit}";
+ }
+
+ $res = $dbr->query( $sql, __METHOD__ );
+
+ if( $dbr->numRows( $res ) == 0 )
+ $this->mResultEmpty = true;
+
+ return $res;
+ }
+
+ function getExtraOptions( $opts ){
+ $opts->consumeValues( array( 'showlinkedto', 'target' ) );
+ $extraOpts = array();
+ $extraOpts['namespace'] = $this->namespaceFilterForm( $opts );
+ $extraOpts['target'] = array( wfMsg( 'recentchangeslinked-page' ),
+ 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 ){
+ 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/SpecialResetpass.php b/includes/specials/SpecialResetpass.php
new file mode 100644
index 00000000..707b941d
--- /dev/null
+++ b/includes/specials/SpecialResetpass.php
@@ -0,0 +1,167 @@
+<?php
+/**
+ * @file
+ * @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' );
+ }
+ }
+
+ /**
+ * Main execution point
+ */
+ function execute( $par ) {
+ global $wgUser, $wgAuth, $wgOut, $wgRequest;
+
+ if( !$wgAuth->allowPasswordChange() ) {
+ $this->error( wfMsg( 'resetpass_forbidden' ) );
+ return;
+ }
+
+ if( $this->mName === null && !$wgRequest->wasPosted() ) {
+ $this->error( wfMsg( 'resetpass_missing' ) );
+ return;
+ }
+
+ if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $wgRequest->getVal( 'token' ) ) ) {
+ $newpass = $wgRequest->getVal( 'wpNewPassword' );
+ $retype = $wgRequest->getVal( 'wpRetype' );
+ try {
+ $this->attemptReset( $newpass, $retype );
+ $wgOut->addWikiMsg( 'resetpass_success' );
+
+ $data = array(
+ 'action' => 'submitlogin',
+ 'wpName' => $this->mName,
+ 'wpPassword' => $newpass,
+ 'returnto' => $wgRequest->getVal( 'returnto' ),
+ );
+ if( $wgRequest->getCheck( 'wpRemember' ) ) {
+ $data['wpRemember'] = 1;
+ }
+ $login = new LoginForm( new FauxRequest( $data, true ) );
+ $login->execute();
+
+ return;
+ } catch( PasswordError $e ) {
+ $this->error( $e->getMessage() );
+ }
+ }
+ $this->showForm();
+ }
+
+ function error( $msg ) {
+ global $wgOut;
+ $wgOut->addHtml( '<div class="errorbox">' .
+ htmlspecialchars( $msg ) .
+ '</div>' );
+ }
+
+ function showForm() {
+ global $wgOut, $wgUser, $wgRequest;
+
+ $wgOut->disallowUserJs();
+
+ $self = SpecialPage::getTitleFor( 'Resetpass' );
+ $form =
+ '<div id="userloginForm">' .
+ wfOpenElement( 'form',
+ array(
+ 'method' => 'post',
+ 'action' => $self->getLocalUrl() ) ) .
+ '<h2>' . wfMsgHtml( 'resetpass_header' ) . '</h2>' .
+ '<div id="userloginprompt">' .
+ wfMsgExt( 'resetpass_text', array( 'parse' ) ) .
+ '</div>' .
+ '<table>' .
+ wfHidden( 'token', $wgUser->editToken() ) .
+ wfHidden( 'wpName', $this->mName ) .
+ wfHidden( 'wpPassword', $this->mTemporaryPassword ) .
+ wfHidden( 'returnto', $wgRequest->getVal( 'returnto' ) ) .
+ $this->pretty( array(
+ array( 'wpName', 'username', 'text', $this->mName ),
+ array( 'wpNewPassword', 'newpassword', 'password', '' ),
+ array( 'wpRetype', 'yourpasswordagain', 'password', '' ),
+ ) ) .
+ '<tr>' .
+ '<td></td>' .
+ '<td>' .
+ Xml::checkLabel( wfMsg( 'remembermypassword' ),
+ 'wpRemember', 'wpRemember',
+ $wgRequest->getCheck( 'wpRemember' ) ) .
+ '</td>' .
+ '</tr>' .
+ '<tr>' .
+ '<td></td>' .
+ '<td>' .
+ wfSubmitButton( wfMsgHtml( 'resetpass_submit' ) ) .
+ '</td>' .
+ '</tr>' .
+ '</table>' .
+ wfCloseElement( 'form' ) .
+ '</div>';
+ $wgOut->addHtml( $form );
+ }
+
+ function pretty( $fields ) {
+ $out = '';
+ foreach( $fields as $list ) {
+ list( $name, $label, $type, $value ) = $list;
+ if( $type == 'text' ) {
+ $field = '<tt>' . htmlspecialchars( $value ) . '</tt>';
+ } 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>';
+ $out .= '<td>';
+ $out .= $field;
+ $out .= '</td>';
+ $out .= '</tr>';
+ }
+ return $out;
+ }
+
+ /**
+ * @throws PasswordError when cannot set the new password because requirements not met.
+ */
+ function attemptReset( $newpass, $retype ) {
+ $user = User::newFromName( $this->mName );
+ if( $user->isAnon() ) {
+ throw new PasswordError( 'no such user' );
+ }
+
+ if( !$user->checkTemporaryPassword( $this->mTemporaryPassword ) ) {
+ throw new PasswordError( wfMsg( 'resetpass_bad_temporary' ) );
+ }
+
+ if( $newpass !== $retype ) {
+ throw new PasswordError( wfMsg( 'badretype' ) );
+ }
+
+ $user->setPassword( $newpass );
+ $user->saveSettings();
+ }
+}
diff --git a/includes/specials/SpecialRevisiondelete.php b/includes/specials/SpecialRevisiondelete.php
new file mode 100644
index 00000000..e94fc222
--- /dev/null
+++ b/includes/specials/SpecialRevisiondelete.php
@@ -0,0 +1,1474 @@
+<?php
+/**
+ * Special page allowing users with the appropriate permissions to view
+ * and hide revisions. Log items can also be hidden.
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+function wfSpecialRevisiondelete( $par = null ) {
+ global $wgOut, $wgRequest, $wgUser;
+ # Handle our many different possible input types
+ $target = $wgRequest->getText( 'target' );
+ $oldid = $wgRequest->getArray( 'oldid' );
+ $artimestamp = $wgRequest->getArray( 'artimestamp' );
+ $logid = $wgRequest->getArray( 'logid' );
+ $img = $wgRequest->getArray( 'oldimage' );
+ $fileid = $wgRequest->getArray( 'fileid' );
+ # For reviewing deleted files...
+ $file = $wgRequest->getVal( 'file' );
+ # If this is a revision, then we need a target page
+ $page = Title::newFromUrl( $target );
+ if( is_null($page) ) {
+ $wgOut->addWikiMsg( 'undelete-header' );
+ return;
+ }
+ # Only one target set at a time please!
+ $i = (bool)$file + (bool)$oldid + (bool)$logid + (bool)$artimestamp + (bool)$fileid + (bool)$img;
+ if( $i !== 1 ) {
+ $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
+ return;
+ }
+ # Logs must have a type given
+ if( $logid && !strpos($page->getDBKey(),'/') ) {
+ $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
+ return;
+ }
+ # Either submit or create our form
+ $form = new RevisionDeleteForm( $page, $oldid, $logid, $artimestamp, $fileid, $img, $file );
+ if( $wgRequest->wasPosted() ) {
+ $form->submit( $wgRequest );
+ } else if( $oldid || $artimestamp ) {
+ $form->showRevs();
+ } else if( $fileid || $img ) {
+ $form->showImages();
+ } else if( $logid ) {
+ $form->showLogItems();
+ }
+ # Show relevant lines from the deletion log. This will show even if said ID
+ # does not exist...might be helpful
+ $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'delete' ) ) . "</h2>\n" );
+ LogEventsList::showLogExtract( $wgOut, 'delete', $page->getPrefixedText() );
+ if( $wgUser->isAllowed( 'suppressionlog' ) ){
+ $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'suppress' ) ) . "</h2>\n" );
+ LogEventsList::showLogExtract( $wgOut, 'suppress', $page->getPrefixedText() );
+ }
+}
+
+/**
+ * Implements the GUI for Revision Deletion.
+ * @ingroup SpecialPage
+ */
+class RevisionDeleteForm {
+ /**
+ * @param Title $page
+ * @param array $oldids
+ * @param array $logids
+ * @param array $artimestamps
+ * @param array $fileids
+ * @param array $img
+ * @param string $file
+ */
+ function __construct( $page, $oldids, $logids, $artimestamps, $fileids, $img, $file ) {
+ global $wgUser, $wgOut;
+
+ $this->page = $page;
+ # For reviewing deleted files...
+ if( $file ) {
+ $oimage = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $page, $file );
+ $oimage->load();
+ // Check if user is allowed to see this file
+ if( !$oimage->userCan(File::DELETED_FILE) ) {
+ $wgOut->permissionRequired( 'suppressrevision' );
+ } else {
+ $this->showFile( $file );
+ }
+ return;
+ }
+ $this->skin = $wgUser->getSkin();
+ # Give a link to the log for this page
+ if( !is_null($this->page) && $this->page->getNamespace() > -1 ) {
+ $links = array();
+
+ $logtitle = SpecialPage::getTitleFor( 'Log' );
+ $links[] = $this->skin->makeKnownLinkObj( $logtitle, wfMsgHtml( 'viewpagelogs' ),
+ wfArrayToCGI( array( 'page' => $this->page->getPrefixedUrl() ) ) );
+ # Give a link to the page history
+ $links[] = $this->skin->makeKnownLinkObj( $this->page, wfMsgHtml( 'pagehist' ),
+ wfArrayToCGI( array( 'action' => 'history' ) ) );
+ # Link to deleted edits
+ if( $wgUser->isAllowed('undelete') ) {
+ $undelete = SpecialPage::getTitleFor( 'Undelete' );
+ $links[] = $this->skin->makeKnownLinkObj( $undelete, wfMsgHtml( 'deletedhist' ),
+ wfArrayToCGI( array( 'target' => $this->page->getPrefixedUrl() ) ) );
+ }
+ # Logs themselves don't have histories or archived revisions
+ $wgOut->setSubtitle( '<p>'.implode($links,' / ').'</p>' );
+ }
+ // At this point, we should only have one of these
+ if( $oldids ) {
+ $this->revisions = $oldids;
+ $hide_content_name = array( 'revdelete-hide-text', 'wpHideText', Revision::DELETED_TEXT );
+ $this->deleteKey='oldid';
+ } else if( $artimestamps ) {
+ $this->archrevs = $artimestamps;
+ $hide_content_name = array( 'revdelete-hide-text', 'wpHideText', Revision::DELETED_TEXT );
+ $this->deleteKey='artimestamp';
+ } else if( $img ) {
+ $this->ofiles = $img;
+ $hide_content_name = array( 'revdelete-hide-image', 'wpHideImage', File::DELETED_FILE );
+ $this->deleteKey='oldimage';
+ } else if( $fileids ) {
+ $this->afiles = $fileids;
+ $hide_content_name = array( 'revdelete-hide-image', 'wpHideImage', File::DELETED_FILE );
+ $this->deleteKey='fileid';
+ } else if( $logids ) {
+ $this->events = $logids;
+ $hide_content_name = array( 'revdelete-hide-name', 'wpHideName', LogPage::DELETED_ACTION );
+ $this->deleteKey='logid';
+ }
+ // Our checkbox messages depends one what we are doing,
+ // e.g. we don't hide "text" for logs or images
+ $this->checks = array(
+ $hide_content_name,
+ array( 'revdelete-hide-comment', 'wpHideComment', Revision::DELETED_COMMENT ),
+ array( 'revdelete-hide-user', 'wpHideUser', Revision::DELETED_USER ) );
+ if( $wgUser->isAllowed('suppressrevision') ) {
+ $this->checks[] = array( 'revdelete-hide-restricted', 'wpHideRestricted', Revision::DELETED_RESTRICTED );
+ }
+ }
+
+ /**
+ * Show a deleted file version requested by the visitor.
+ */
+ private function showFile( $key ) {
+ global $wgOut, $wgRequest;
+ $wgOut->disable();
+
+ # We mustn't allow the output to be Squid cached, otherwise
+ # if an admin previews a deleted image, and it's cached, then
+ # a user without appropriate permissions can toddle off and
+ # nab the image, and Squid will serve it
+ $wgRequest->response()->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
+ $wgRequest->response()->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
+ $wgRequest->response()->header( 'Pragma: no-cache' );
+
+ $store = FileStore::get( 'deleted' );
+ $store->stream( $key );
+ }
+
+ /**
+ * This lets a user set restrictions for live and archived revisions
+ */
+ function showRevs() {
+ global $wgOut, $wgUser, $action;
+
+ $UserAllowed = true;
+
+ $count = ($this->deleteKey=='oldid') ?
+ count($this->revisions) : count($this->archrevs);
+ $wgOut->addWikiMsg( 'revdelete-selected', $this->page->getPrefixedText(), $count );
+
+ $bitfields = 0;
+ $wgOut->addHtml( "<ul>" );
+
+ $where = $revObjs = array();
+ $dbr = wfGetDB( DB_SLAVE );
+
+ $revisions = 0;
+ // Live revisions...
+ if( $this->deleteKey=='oldid' ) {
+ // Run through and pull all our data in one query
+ foreach( $this->revisions as $revid ) {
+ $where[] = intval($revid);
+ }
+ $whereClause = 'rev_id IN(' . implode(',',$where) . ')';
+ $result = $dbr->select( array('revision','page'), '*',
+ array( 'rev_page' => $this->page->getArticleID(),
+ $whereClause, 'rev_page = page_id' ),
+ __METHOD__ );
+ while( $row = $dbr->fetchObject( $result ) ) {
+ $revObjs[$row->rev_id] = new Revision( $row );
+ }
+ foreach( $this->revisions as $revid ) {
+ // Hiding top revisison is bad
+ if( !isset($revObjs[$revid]) || $revObjs[$revid]->isCurrent() ) {
+ continue;
+ } else if( !$revObjs[$revid]->userCan(Revision::DELETED_RESTRICTED) ) {
+ // If a rev is hidden from sysops
+ if( $action != 'submit') {
+ $wgOut->permissionRequired( 'suppressrevision' );
+ return;
+ }
+ $UserAllowed = false;
+ }
+ $revisions++;
+ $wgOut->addHtml( $this->historyLine( $revObjs[$revid] ) );
+ $bitfields |= $revObjs[$revid]->mDeleted;
+ }
+ // The archives...
+ } else {
+ // Run through and pull all our data in one query
+ foreach( $this->archrevs as $timestamp ) {
+ $where[] = $dbr->addQuotes( $timestamp );
+ }
+ $whereClause = 'ar_timestamp IN(' . implode(',',$where) . ')';
+ $result = $dbr->select( 'archive', '*',
+ array( 'ar_namespace' => $this->page->getNamespace(),
+ 'ar_title' => $this->page->getDBKey(),
+ $whereClause ),
+ __METHOD__ );
+ while( $row = $dbr->fetchObject( $result ) ) {
+ $revObjs[$row->ar_timestamp] = new Revision( array(
+ 'page' => $this->page->getArticleId(),
+ 'id' => $row->ar_rev_id,
+ 'text' => $row->ar_text_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,
+ 'text_id' => $row->ar_text_id,
+ 'deleted' => $row->ar_deleted,
+ 'len' => $row->ar_len) );
+ }
+ foreach( $this->archrevs as $timestamp ) {
+ if( !isset($revObjs[$timestamp]) ) {
+ continue;
+ } else if( !$revObjs[$timestamp]->userCan(Revision::DELETED_RESTRICTED) ) {
+ // If a rev is hidden from sysops
+ if( $action != 'submit') {
+ $wgOut->permissionRequired( 'suppressrevision' );
+ return;
+ }
+ $UserAllowed = false;
+ }
+ $revisions++;
+ $wgOut->addHtml( $this->historyLine( $revObjs[$timestamp] ) );
+ $bitfields |= $revObjs[$timestamp]->mDeleted;
+ }
+ }
+ if( !$revisions ) {
+ $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
+ return;
+ }
+
+ $wgOut->addHtml( "</ul>" );
+
+ $wgOut->addWikiMsg( 'revdelete-text' );
+
+ // Normal sysops can always see what they did, but can't always change it
+ if( !$UserAllowed ) return;
+
+ $items = array(
+ Xml::inputLabel( wfMsg( 'revdelete-log' ), 'wpReason', 'wpReason', 60 ),
+ Xml::submitButton( wfMsg( 'revdelete-submit' ) )
+ );
+ $hidden = array(
+ Xml::hidden( 'wpEditToken', $wgUser->editToken() ),
+ Xml::hidden( 'target', $this->page->getPrefixedText() ),
+ Xml::hidden( 'type', $this->deleteKey )
+ );
+ if( $this->deleteKey=='oldid' ) {
+ foreach( $revObjs as $rev )
+ $hidden[] = Xml::hidden( 'oldid[]', $rev->getId() );
+ } else {
+ foreach( $revObjs as $rev )
+ $hidden[] = Xml::hidden( 'artimestamp[]', $rev->getTimestamp() );
+ }
+ $special = SpecialPage::getTitleFor( 'Revisiondelete' );
+ $wgOut->addHtml(
+ Xml::openElement( 'form', array( 'method' => 'post', 'action' => $special->getLocalUrl( 'action=submit' ),
+ 'id' => 'mw-revdel-form-revisions' ) ) .
+ Xml::openElement( 'fieldset' ) .
+ xml::element( 'legend', null, wfMsg( 'revdelete-legend' ) )
+ );
+ // 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 ) ) );
+ }
+ foreach( $items as $item ) {
+ $wgOut->addHtml( Xml::tags( 'p', null, $item ) );
+ }
+ foreach( $hidden as $item ) {
+ $wgOut->addHtml( $item );
+ }
+ $wgOut->addHtml(
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' ) . "\n"
+ );
+
+ }
+
+ /**
+ * This lets a user set restrictions for archived images
+ */
+ function showImages() {
+ // What is $action doing here???
+ global $wgOut, $wgUser, $action, $wgLang;
+
+ $UserAllowed = true;
+
+ $count = ($this->deleteKey=='oldimage') ? count($this->ofiles) : count($this->afiles);
+ $wgOut->addWikiMsg( 'revdelete-selected',
+ $this->page->getPrefixedText(),
+ $wgLang->formatNum($count) );
+
+ $bitfields = 0;
+ $wgOut->addHtml( "<ul>" );
+
+ $where = $filesObjs = array();
+ $dbr = wfGetDB( DB_SLAVE );
+ // Live old revisions...
+ $revisions = 0;
+ 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() );
+ }
+ $whereClause = 'oi_archive_name IN(' . implode(',',$where) . ')';
+ $result = $dbr->select( 'oldimage', '*',
+ array( 'oi_name' => $this->page->getDbKey(),
+ $whereClause ),
+ __METHOD__ );
+ while( $row = $dbr->fetchObject( $result ) ) {
+ $filesObjs[$row->oi_archive_name] = RepoGroup::singleton()->getLocalRepo()->newFileFromRow( $row );
+ $filesObjs[$row->oi_archive_name]->user = $row->oi_user;
+ $filesObjs[$row->oi_archive_name]->user_text = $row->oi_user_text;
+ }
+ // Check through our images
+ foreach( $this->ofiles as $timestamp ) {
+ $archivename = $timestamp.'!'.$this->page->getDbKey();
+ if( !isset($filesObjs[$archivename]) ) {
+ continue;
+ } else if( !$filesObjs[$archivename]->userCan(File::DELETED_RESTRICTED) ) {
+ // If a rev is hidden from sysops
+ if( $action != 'submit' ) {
+ $wgOut->permissionRequired( 'suppressrevision' );
+ return;
+ }
+ $UserAllowed = false;
+ }
+ $revisions++;
+ // Inject history info
+ $wgOut->addHtml( $this->fileLine( $filesObjs[$archivename] ) );
+ $bitfields |= $filesObjs[$archivename]->deleted;
+ }
+ // Archived files...
+ } else {
+ // Run through and pull all our data in one query
+ foreach( $this->afiles as $id ) {
+ $where[] = intval($id);
+ }
+ $whereClause = 'fa_id IN(' . implode(',',$where) . ')';
+ $result = $dbr->select( 'filearchive', '*',
+ array( 'fa_name' => $this->page->getDbKey(),
+ $whereClause ),
+ __METHOD__ );
+ while( $row = $dbr->fetchObject( $result ) ) {
+ $filesObjs[$row->fa_id] = ArchivedFile::newFromRow( $row );
+ }
+
+ foreach( $this->afiles as $fileid ) {
+ if( !isset($filesObjs[$fileid]) ) {
+ continue;
+ } else if( !$filesObjs[$fileid]->userCan(File::DELETED_RESTRICTED) ) {
+ // If a rev is hidden from sysops
+ if( $action != 'submit' ) {
+ $wgOut->permissionRequired( 'suppressrevision' );
+ return;
+ }
+ $UserAllowed = false;
+ }
+ $revisions++;
+ // Inject history info
+ $wgOut->addHtml( $this->archivedfileLine( $filesObjs[$fileid] ) );
+ $bitfields |= $filesObjs[$fileid]->deleted;
+ }
+ }
+ if( !$revisions ) {
+ $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
+ return;
+ }
+
+ $wgOut->addHtml( "</ul>" );
+
+ $wgOut->addWikiMsg('revdelete-text' );
+ //Normal sysops can always see what they did, but can't always change it
+ if( !$UserAllowed ) return;
+
+ $items = array(
+ Xml::inputLabel( wfMsg( 'revdelete-log' ), 'wpReason', 'wpReason', 60 ),
+ Xml::submitButton( wfMsg( 'revdelete-submit' ) )
+ );
+ $hidden = array(
+ Xml::hidden( 'wpEditToken', $wgUser->editToken() ),
+ Xml::hidden( 'target', $this->page->getPrefixedText() ),
+ Xml::hidden( 'type', $this->deleteKey )
+ );
+ if( $this->deleteKey=='oldimage' ) {
+ foreach( $this->ofiles as $filename )
+ $hidden[] = Xml::hidden( 'oldimage[]', $filename );
+ } else {
+ foreach( $this->afiles as $fileid )
+ $hidden[] = Xml::hidden( 'fileid[]', $fileid );
+ }
+ $special = SpecialPage::getTitleFor( 'Revisiondelete' );
+ $wgOut->addHtml(
+ Xml::openElement( 'form', array( 'method' => 'post', 'action' => $special->getLocalUrl( 'action=submit' ),
+ 'id' => 'mw-revdel-form-filerevisions' ) ) .
+ Xml::fieldset( wfMsg( 'revdelete-legend' ) )
+ );
+ // 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 ) ) );
+ }
+ foreach( $items as $item ) {
+ $wgOut->addHtml( "<p>$item</p>" );
+ }
+ foreach( $hidden as $item ) {
+ $wgOut->addHtml( $item );
+ }
+
+ $wgOut->addHtml(
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' ) . "\n"
+ );
+ }
+
+ /**
+ * This lets a user set restrictions for log items
+ */
+ function showLogItems() {
+ global $wgOut, $wgUser, $action, $wgMessageCache, $wgLang;
+
+ $UserAllowed = true;
+ $wgOut->addWikiMsg( 'logdelete-selected', $wgLang->formatNum( count($this->events) ) );
+
+ $bitfields = 0;
+ $wgOut->addHtml( "<ul>" );
+
+ $where = $logRows = array();
+ $dbr = wfGetDB( DB_SLAVE );
+ // Run through and pull all our data in one query
+ $logItems = 0;
+ foreach( $this->events as $logid ) {
+ $where[] = intval($logid);
+ }
+ list($log,$logtype) = explode( '/',$this->page->getDBKey(), 2 );
+ $whereClause = "log_type = '$logtype' AND log_id IN(" . implode(',',$where) . ")";
+ $result = $dbr->select( 'logging', '*',
+ array( $whereClause ),
+ __METHOD__ );
+ while( $row = $dbr->fetchObject( $result ) ) {
+ $logRows[$row->log_id] = $row;
+ }
+ $wgMessageCache->loadAllMessages();
+ foreach( $this->events as $logid ) {
+ // Don't hide from oversight log!!!
+ if( !isset( $logRows[$logid] ) || $logRows[$logid]->log_type=='suppress' ) {
+ continue;
+ } else if( !LogEventsList::userCan( $logRows[$logid],Revision::DELETED_RESTRICTED) ) {
+ // If an event is hidden from sysops
+ if( $action != 'submit') {
+ $wgOut->permissionRequired( 'suppressrevision' );
+ return;
+ }
+ $UserAllowed = false;
+ }
+ $logItems++;
+ $wgOut->addHtml( $this->logLine( $logRows[$logid] ) );
+ $bitfields |= $logRows[$logid]->log_deleted;
+ }
+ if( !$logItems ) {
+ $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
+ return;
+ }
+
+ $wgOut->addHtml( "</ul>" );
+
+ $wgOut->addWikiMsg( 'revdelete-text' );
+ // Normal sysops can always see what they did, but can't always change it
+ if( !$UserAllowed ) return;
+
+ $items = array(
+ Xml::inputLabel( wfMsg( 'revdelete-log' ), 'wpReason', 'wpReason', 60 ),
+ Xml::submitButton( wfMsg( 'revdelete-submit' ) ) );
+ $hidden = array(
+ Xml::hidden( 'wpEditToken', $wgUser->editToken() ),
+ Xml::hidden( 'target', $this->page->getPrefixedText() ),
+ Xml::hidden( 'type', $this->deleteKey ) );
+ foreach( $this->events as $logid ) {
+ $hidden[] = Xml::hidden( 'logid[]', $logid );
+ }
+
+ $special = SpecialPage::getTitleFor( 'Revisiondelete' );
+ $wgOut->addHtml(
+ Xml::openElement( 'form', array( 'method' => 'post', 'action' => $special->getLocalUrl( 'action=submit' ),
+ 'id' => 'mw-revdel-form-logs' ) ) .
+ Xml::fieldset( wfMsg( 'revdelete-legend' ) )
+ );
+ // 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 ) ) );
+ }
+ foreach( $items as $item ) {
+ $wgOut->addHtml( "<p>$item</p>" );
+ }
+ foreach( $hidden as $item ) {
+ $wgOut->addHtml( $item );
+ }
+
+ $wgOut->addHtml(
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' ) . "\n"
+ );
+ }
+
+ /**
+ * @param Revision $rev
+ * @returns string
+ */
+ private function historyLine( $rev ) {
+ global $wgLang;
+
+ $date = $wgLang->timeanddate( $rev->getTimestamp() );
+ $difflink = $del = '';
+ // Live revisions
+ if( $this->deleteKey=='oldid' ) {
+ $revlink = $this->skin->makeLinkObj( $this->page, $date, 'oldid=' . $rev->getId() );
+ $difflink = '(' . $this->skin->makeKnownLinkObj( $this->page, wfMsgHtml('diff'),
+ 'diff=' . $rev->getId() . '&oldid=prev' ) . ')';
+ // Archived revisions
+ } else {
+ $undelete = SpecialPage::getTitleFor( 'Undelete' );
+ $target = $this->page->getPrefixedText();
+ $revlink = $this->skin->makeLinkObj( $undelete, $date,
+ "target=$target&timestamp=" . $rev->getTimestamp() );
+ $difflink = '(' . $this->skin->makeKnownLinkObj( $undelete, wfMsgHtml('diff'),
+ "target=$target&diff=prev&timestamp=" . $rev->getTimestamp() ) . ')';
+ }
+
+ if( $rev->isDeleted(Revision::DELETED_TEXT) ) {
+ $revlink = '<span class="history-deleted">'.$revlink.'</span>';
+ $del = ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
+ if( !$rev->userCan(Revision::DELETED_TEXT) ) {
+ $revlink = '<span class="history-deleted">'.$date.'</span>';
+ $difflink = '(' . wfMsgHtml('diff') . ')';
+ }
+ }
+
+ return "<li> $difflink $revlink ".$this->skin->revUserLink( $rev )." ".$this->skin->revComment( $rev )."$del</li>";
+ }
+
+ /**
+ * @param File $file
+ * @returns string
+ */
+ private function fileLine( $file ) {
+ global $wgLang, $wgTitle;
+
+ $target = $this->page->getPrefixedText();
+ $date = $wgLang->timeanddate( $file->getTimestamp(), true );
+
+ $del = '';
+ # Hidden files...
+ if( $file->isDeleted(File::DELETED_FILE) ) {
+ $del = ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
+ if( !$file->userCan(File::DELETED_FILE) ) {
+ $pageLink = $date;
+ } else {
+ $pageLink = $this->skin->makeKnownLinkObj( $wgTitle, $date,
+ "target=$target&file=$file->sha1.".$file->getExtension() );
+ }
+ $pageLink = '<span class="history-deleted">' . $pageLink . '</span>';
+ # Regular files...
+ } else {
+ $url = $file->getUrlRel();
+ $pageLink = "<a href=\"{$url}\">{$date}</a>";
+ }
+
+ $data = wfMsg( 'widthheight',
+ $wgLang->formatNum( $file->getWidth() ),
+ $wgLang->formatNum( $file->getHeight() ) ) .
+ ' (' . wfMsgExt( 'nbytes', 'parsemag', $wgLang->formatNum( $file->getSize() ) ) . ')';
+ $data = htmlspecialchars( $data );
+
+ return "<li>$pageLink ".$this->fileUserTools( $file )." $data ".$this->fileComment( $file )."$del</li>";
+ }
+
+ /**
+ * @param ArchivedFile $file
+ * @returns string
+ */
+ private function archivedfileLine( $file ) {
+ global $wgLang, $wgTitle;
+
+ $target = $this->page->getPrefixedText();
+ $date = $wgLang->timeanddate( $file->getTimestamp(), true );
+
+ $undelete = SpecialPage::getTitleFor( 'Undelete' );
+ $pageLink = $this->skin->makeKnownLinkObj( $undelete, $date, "target=$target&file={$file->getKey()}" );
+
+ $del = '';
+ if( $file->isDeleted(File::DELETED_FILE) ) {
+ $del = ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
+ }
+
+ $data = wfMsg( 'widthheight',
+ $wgLang->formatNum( $file->getWidth() ),
+ $wgLang->formatNum( $file->getHeight() ) ) .
+ ' (' . wfMsgExt( 'nbytes', 'parsemag', $wgLang->formatNum( $file->getSize() ) ) . ')';
+ $data = htmlspecialchars( $data );
+
+ return "<li> $pageLink ".$this->fileUserTools( $file )." $data ".$this->fileComment( $file )."$del</li>";
+ }
+
+ /**
+ * @param Array $row row
+ * @returns string
+ */
+ private function logLine( $row ) {
+ global $wgLang;
+
+ $date = $wgLang->timeanddate( $row->log_timestamp );
+ $paramArray = LogPage::extractParams( $row->log_params );
+ $title = Title::makeTitle( $row->log_namespace, $row->log_title );
+
+ $logtitle = SpecialPage::getTitleFor( 'Log' );
+ $loglink = $this->skin->makeKnownLinkObj( $logtitle, wfMsgHtml( 'log' ),
+ wfArrayToCGI( array( 'page' => $title->getPrefixedUrl() ) ) );
+ // Action text
+ if( !LogEventsList::userCan($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, true );
+ if( $row->log_deleted & LogPage::DELETED_ACTION )
+ $action = '<span class="history-deleted">' . $action . '</span>';
+ }
+ // User links
+ $userLink = $this->skin->userLink( $row->log_user, User::WhoIs($row->log_user) );
+ if( LogEventsList::isDeleted($row,LogPage::DELETED_USER) ) {
+ $userLink = '<span class="history-deleted">' . $userLink . '</span>';
+ }
+ // Comment
+ $comment = $wgLang->getDirMark() . $this->skin->commentBlock( $row->log_comment );
+ if( LogEventsList::isDeleted($row,LogPage::DELETED_COMMENT) ) {
+ $comment = '<span class="history-deleted">' . $comment . '</span>';
+ }
+ return "<li>($loglink) $date $userLink $action $comment</li>";
+ }
+
+ /**
+ * Generate a user tool link cluster if the current user is allowed to view it
+ * @param ArchivedFile $file
+ * @return string HTML
+ */
+ private function fileUserTools( $file ) {
+ if( $file->userCan( Revision::DELETED_USER ) ) {
+ $link = $this->skin->userLink( $file->user, $file->user_text ) .
+ $this->skin->userToolLinks( $file->user, $file->user_text );
+ } else {
+ $link = wfMsgHtml( 'rev-deleted-user' );
+ }
+ if( $file->isDeleted( Revision::DELETED_USER ) ) {
+ return '<span class="history-deleted">' . $link . '</span>';
+ }
+ return $link;
+ }
+
+ /**
+ * Wrap and format the given file's comment block, if the current
+ * user is allowed to view it.
+ *
+ * @param ArchivedFile $file
+ * @return string HTML
+ */
+ private function fileComment( $file ) {
+ if( $file->userCan( File::DELETED_COMMENT ) ) {
+ $block = $this->skin->commentBlock( $file->description );
+ } else {
+ $block = ' ' . wfMsgHtml( 'rev-deleted-comment' );
+ }
+ if( $file->isDeleted( File::DELETED_COMMENT ) ) {
+ return "<span class=\"history-deleted\">$block</span>";
+ }
+ return $block;
+ }
+
+ /**
+ * @param WebRequest $request
+ */
+ function submit( $request ) {
+ global $wgUser, $wgOut;
+
+ $bitfield = $this->extractBitfield( $request );
+ $comment = $request->getText( 'wpReason' );
+ # Can the user set this field?
+ if( $bitfield & Revision::DELETED_RESTRICTED && !$wgUser->isAllowed('suppressrevision') ) {
+ $wgOut->permissionRequired( 'suppressrevision' );
+ return false;
+ }
+ # If the save went through, go to success message. Otherwise
+ # bounce back to form...
+ if( $this->save( $bitfield, $comment, $this->page ) ) {
+ $this->success();
+ } else if( $request->getCheck( 'oldid' ) || $request->getCheck( 'artimestamp' ) ) {
+ return $this->showRevs();
+ } else if( $request->getCheck( 'logid' ) ) {
+ return $this->showLogs();
+ } else if( $request->getCheck( 'oldimage' ) || $request->getCheck( 'fileid' ) ) {
+ return $this->showImages();
+ }
+ }
+
+ private function success() {
+ global $wgOut;
+
+ $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) );
+
+ $wrap = '<span class="success">$1</span>';
+
+ if( $this->deleteKey=='logid' ) {
+ $wgOut->wrapWikiMsg( $wrap, 'logdelete-success' );
+ $this->showLogItems();
+ } else if( $this->deleteKey=='oldid' || $this->deleteKey=='artimestamp' ) {
+ $wgOut->wrapWikiMsg( $wrap, 'revdelete-success' );
+ $this->showRevs();
+ } else if( $this->deleteKey=='fileid' ) {
+ $wgOut->wrapWikiMsg( $wrap, 'revdelete-success' );
+ $this->showImages();
+ } else if( $this->deleteKey=='oldimage' ) {
+ $wgOut->wrapWikiMsg( $wrap, 'revdelete-success' );
+ $this->showImages();
+ }
+ }
+
+ /**
+ * Put together a rev_deleted bitfield from the submitted checkboxes
+ * @param WebRequest $request
+ * @return int
+ */
+ private function extractBitfield( $request ) {
+ $bitfield = 0;
+ foreach( $this->checks as $item ) {
+ list( /* message */ , $name, $field ) = $item;
+ if( $request->getCheck( $name ) ) {
+ $bitfield |= $field;
+ }
+ }
+ return $bitfield;
+ }
+
+ private function save( $bitfield, $reason, $title ) {
+ $dbw = wfGetDB( DB_MASTER );
+ // Don't allow simply locking the interface for no reason
+ if( $bitfield == Revision::DELETED_RESTRICTED ) {
+ $bitfield = 0;
+ }
+ $deleter = new RevisionDeleter( $dbw );
+ // By this point, only one of the below should be set
+ if( isset($this->revisions) ) {
+ return $deleter->setRevVisibility( $title, $this->revisions, $bitfield, $reason );
+ } else if( isset($this->archrevs) ) {
+ return $deleter->setArchiveVisibility( $title, $this->archrevs, $bitfield, $reason );
+ } else if( isset($this->ofiles) ) {
+ return $deleter->setOldImgVisibility( $title, $this->ofiles, $bitfield, $reason );
+ } else if( isset($this->afiles) ) {
+ return $deleter->setArchFileVisibility( $title, $this->afiles, $bitfield, $reason );
+ } else if( isset($this->events) ) {
+ return $deleter->setEventVisibility( $title, $this->events, $bitfield, $reason );
+ }
+ }
+}
+
+/**
+ * Implements the actions for Revision Deletion.
+ * @ingroup SpecialPage
+ */
+class RevisionDeleter {
+ function __construct( $db ) {
+ $this->dbw = $db;
+ }
+
+ /**
+ * @param $title, the page these events apply to
+ * @param array $items list of revision ID numbers
+ * @param int $bitfield new rev_deleted value
+ * @param string $comment Comment for log records
+ */
+ function setRevVisibility( $title, $items, $bitfield, $comment ) {
+ global $wgOut;
+
+ $userAllowedAll = $success = true;
+ $revIDs = array();
+ $revCount = 0;
+ // Run through and pull all our data in one query
+ foreach( $items as $revid ) {
+ $where[] = intval($revid);
+ }
+ $whereClause = 'rev_id IN(' . implode(',',$where) . ')';
+ $result = $this->dbw->select( 'revision', '*',
+ array( 'rev_page' => $title->getArticleID(),
+ $whereClause ),
+ __METHOD__ );
+ while( $row = $this->dbw->fetchObject( $result ) ) {
+ $revObjs[$row->rev_id] = new Revision( $row );
+ }
+ // To work!
+ foreach( $items as $revid ) {
+ if( !isset($revObjs[$revid]) || $revObjs[$revid]->isCurrent() ) {
+ $success = false;
+ continue; // Must exist
+ } else if( !$revObjs[$revid]->userCan(Revision::DELETED_RESTRICTED) ) {
+ $userAllowedAll=false;
+ continue;
+ }
+ // For logging, maintain a count of revisions
+ if( $revObjs[$revid]->mDeleted != $bitfield ) {
+ $revCount++;
+ $revIDs[]=$revid;
+
+ $this->updateRevision( $revObjs[$revid], $bitfield );
+ $this->updateRecentChangesEdits( $revObjs[$revid], $bitfield, false );
+ }
+ }
+ // Clear caches...
+ // Don't log or touch if nothing changed
+ if( $revCount > 0 ) {
+ $this->updateLog( $title, $revCount, $bitfield, $revObjs[$revid]->mDeleted,
+ $comment, $title, 'oldid', $revIDs );
+ $this->updatePage( $title );
+ }
+ // Where all revs allowed to be set?
+ if( !$userAllowedAll ) {
+ //FIXME: still might be confusing???
+ $wgOut->permissionRequired( 'suppressrevision' );
+ return false;
+ }
+
+ return $success;
+ }
+
+ /**
+ * @param $title, the page these events apply to
+ * @param array $items list of revision ID numbers
+ * @param int $bitfield new rev_deleted value
+ * @param string $comment Comment for log records
+ */
+ function setArchiveVisibility( $title, $items, $bitfield, $comment ) {
+ global $wgOut;
+
+ $userAllowedAll = $success = true;
+ $count = 0;
+ $Id_set = array();
+ // Run through and pull all our data in one query
+ foreach( $items as $timestamp ) {
+ $where[] = $this->dbw->addQuotes( $timestamp );
+ }
+ $whereClause = 'ar_timestamp IN(' . implode(',',$where) . ')';
+ $result = $this->dbw->select( 'archive', '*',
+ array( 'ar_namespace' => $title->getNamespace(),
+ 'ar_title' => $title->getDBKey(),
+ $whereClause ),
+ __METHOD__ );
+ while( $row = $this->dbw->fetchObject( $result ) ) {
+ $revObjs[$row->ar_timestamp] = new Revision( array(
+ 'page' => $title->getArticleId(),
+ 'id' => $row->ar_rev_id,
+ 'text' => $row->ar_text_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,
+ 'text_id' => $row->ar_text_id,
+ 'deleted' => $row->ar_deleted,
+ 'len' => $row->ar_len) );
+ }
+ // To work!
+ foreach( $items as $timestamp ) {
+ // This will only select the first revision with this timestamp.
+ // Since they are all selected/deleted at once, we can just check the
+ // permissions of one. UPDATE is done via timestamp, so all revs are set.
+ if( !is_object($revObjs[$timestamp]) ) {
+ $success = false;
+ continue; // Must exist
+ } else if( !$revObjs[$timestamp]->userCan(Revision::DELETED_RESTRICTED) ) {
+ $userAllowedAll=false;
+ continue;
+ }
+ // Which revisions did we change anything about?
+ if( $revObjs[$timestamp]->mDeleted != $bitfield ) {
+ $Id_set[]=$timestamp;
+ $count++;
+
+ $this->updateArchive( $revObjs[$timestamp], $title, $bitfield );
+ }
+ }
+ // For logging, maintain a count of revisions
+ if( $count > 0 ) {
+ $this->updateLog( $title, $count, $bitfield, $revObjs[$timestamp]->mDeleted,
+ $comment, $title, 'artimestamp', $Id_set );
+ }
+ // Where all revs allowed to be set?
+ if( !$userAllowedAll ) {
+ $wgOut->permissionRequired( 'suppressrevision' );
+ return false;
+ }
+
+ return $success;
+ }
+
+ /**
+ * @param $title, the page these events apply to
+ * @param array $items list of revision ID numbers
+ * @param int $bitfield new rev_deleted value
+ * @param string $comment Comment for log records
+ */
+ function setOldImgVisibility( $title, $items, $bitfield, $comment ) {
+ global $wgOut;
+
+ $userAllowedAll = $success = true;
+ $count = 0;
+ $set = array();
+ // Run through and pull all our data in one query
+ foreach( $items as $timestamp ) {
+ $where[] = $this->dbw->addQuotes( $timestamp.'!'.$title->getDbKey() );
+ }
+ $whereClause = 'oi_archive_name IN(' . implode(',',$where) . ')';
+ $result = $this->dbw->select( 'oldimage', '*',
+ array( 'oi_name' => $title->getDbKey(),
+ $whereClause ),
+ __METHOD__ );
+ while( $row = $this->dbw->fetchObject( $result ) ) {
+ $filesObjs[$row->oi_archive_name] = RepoGroup::singleton()->getLocalRepo()->newFileFromRow( $row );
+ $filesObjs[$row->oi_archive_name]->user = $row->oi_user;
+ $filesObjs[$row->oi_archive_name]->user_text = $row->oi_user_text;
+ }
+ // To work!
+ foreach( $items as $timestamp ) {
+ $archivename = $timestamp.'!'.$title->getDbKey();
+ if( !isset($filesObjs[$archivename]) ) {
+ $success = false;
+ continue; // Must exist
+ } else if( !$filesObjs[$archivename]->userCan(File::DELETED_RESTRICTED) ) {
+ $userAllowedAll=false;
+ continue;
+ }
+
+ $transaction = true;
+ // Which revisions did we change anything about?
+ if( $filesObjs[$archivename]->deleted != $bitfield ) {
+ $count++;
+
+ $this->dbw->begin();
+ $this->updateOldFiles( $filesObjs[$archivename], $bitfield );
+ // If this image is currently hidden...
+ if( $filesObjs[$archivename]->deleted & File::DELETED_FILE ) {
+ if( $bitfield & File::DELETED_FILE ) {
+ # Leave it alone if we are not changing this...
+ $set[]=$archivename;
+ $transaction = true;
+ } else {
+ # We are moving this out
+ $transaction = $this->makeOldImagePublic( $filesObjs[$archivename] );
+ $set[]=$transaction;
+ }
+ // Is it just now becoming hidden?
+ } else if( $bitfield & File::DELETED_FILE ) {
+ $transaction = $this->makeOldImagePrivate( $filesObjs[$archivename] );
+ $set[]=$transaction;
+ } else {
+ $set[]=$timestamp;
+ }
+ // If our file operations fail, then revert back the db
+ if( $transaction==false ) {
+ $this->dbw->rollback();
+ return false;
+ }
+ $this->dbw->commit();
+ }
+ }
+
+ // Log if something was changed
+ if( $count > 0 ) {
+ $this->updateLog( $title, $count, $bitfield, $filesObjs[$archivename]->deleted,
+ $comment, $title, 'oldimage', $set );
+ # Purge page/history
+ $file = wfLocalFile( $title );
+ $file->purgeCache();
+ $file->purgeHistory();
+ # Invalidate cache for all pages using this file
+ $update = new HTMLCacheUpdate( $title, 'imagelinks' );
+ $update->doUpdate();
+ }
+ // Where all revs allowed to be set?
+ if( !$userAllowedAll ) {
+ $wgOut->permissionRequired( 'suppressrevision' );
+ return false;
+ }
+
+ return $success;
+ }
+
+ /**
+ * @param $title, the page these events apply to
+ * @param array $items list of revision ID numbers
+ * @param int $bitfield new rev_deleted value
+ * @param string $comment Comment for log records
+ */
+ function setArchFileVisibility( $title, $items, $bitfield, $comment ) {
+ global $wgOut;
+
+ $userAllowedAll = $success = true;
+ $count = 0;
+ $Id_set = array();
+
+ // Run through and pull all our data in one query
+ foreach( $items as $id ) {
+ $where[] = intval($id);
+ }
+ $whereClause = 'fa_id IN(' . implode(',',$where) . ')';
+ $result = $this->dbw->select( 'filearchive', '*',
+ array( 'fa_name' => $title->getDbKey(),
+ $whereClause ),
+ __METHOD__ );
+ while( $row = $this->dbw->fetchObject( $result ) ) {
+ $filesObjs[$row->fa_id] = ArchivedFile::newFromRow( $row );
+ }
+ // To work!
+ foreach( $items as $fileid ) {
+ if( !isset($filesObjs[$fileid]) ) {
+ $success = false;
+ continue; // Must exist
+ } else if( !$filesObjs[$fileid]->userCan(File::DELETED_RESTRICTED) ) {
+ $userAllowedAll=false;
+ continue;
+ }
+ // Which revisions did we change anything about?
+ if( $filesObjs[$fileid]->deleted != $bitfield ) {
+ $Id_set[]=$fileid;
+ $count++;
+
+ $this->updateArchFiles( $filesObjs[$fileid], $bitfield );
+ }
+ }
+ // Log if something was changed
+ if( $count > 0 ) {
+ $this->updateLog( $title, $count, $bitfield, $comment,
+ $filesObjs[$fileid]->deleted, $title, 'fileid', $Id_set );
+ }
+ // Where all revs allowed to be set?
+ if( !$userAllowedAll ) {
+ $wgOut->permissionRequired( 'suppressrevision' );
+ return false;
+ }
+
+ return $success;
+ }
+
+ /**
+ * @param $title, the log page these events apply to
+ * @param array $items list of log ID numbers
+ * @param int $bitfield new log_deleted value
+ * @param string $comment Comment for log records
+ */
+ function setEventVisibility( $title, $items, $bitfield, $comment ) {
+ global $wgOut;
+
+ $userAllowedAll = $success = true;
+ $count = 0;
+ $log_Ids = array();
+
+ // Run through and pull all our data in one query
+ foreach( $items as $logid ) {
+ $where[] = intval($logid);
+ }
+ list($log,$logtype) = explode( '/',$title->getDBKey(), 2 );
+ $whereClause = "log_type ='$logtype' AND log_id IN(" . implode(',',$where) . ")";
+ $result = $this->dbw->select( 'logging', '*',
+ array( $whereClause ),
+ __METHOD__ );
+ while( $row = $this->dbw->fetchObject( $result ) ) {
+ $logRows[$row->log_id] = $row;
+ }
+ // To work!
+ foreach( $items as $logid ) {
+ if( !isset($logRows[$logid]) ) {
+ $success = false;
+ continue; // Must exist
+ } else if( !LogEventsList::userCan($logRows[$logid], LogPage::DELETED_RESTRICTED)
+ || $logRows[$logid]->log_type == 'suppress' ) {
+ // Don't hide from oversight log!!!
+ $userAllowedAll=false;
+ continue;
+ }
+ // Which logs did we change anything about?
+ if( $logRows[$logid]->log_deleted != $bitfield ) {
+ $log_Ids[]=$logid;
+ $count++;
+
+ $this->updateLogs( $logRows[$logid], $bitfield );
+ $this->updateRecentChangesLog( $logRows[$logid], $bitfield, true );
+ }
+ }
+ // Don't log or touch if nothing changed
+ if( $count > 0 ) {
+ $this->updateLog( $title, $count, $bitfield, $logRows[$logid]->log_deleted,
+ $comment, $title, 'logid', $log_Ids );
+ }
+ // Were all revs allowed to be set?
+ if( !$userAllowedAll ) {
+ $wgOut->permissionRequired( 'suppressrevision' );
+ return false;
+ }
+
+ return $success;
+ }
+
+ /**
+ * Moves an image to a safe private location
+ * Caller is responsible for clearing caches
+ * @param File $oimage
+ * @returns mixed, timestamp string on success, false on failure
+ */
+ function makeOldImagePrivate( $oimage ) {
+ $transaction = new FSTransaction();
+ if( !FileStore::lock() ) {
+ wfDebug( __METHOD__.": failed to acquire file store lock, aborting\n" );
+ return false;
+ }
+ $oldpath = $oimage->getArchivePath() . DIRECTORY_SEPARATOR . $oimage->archive_name;
+ // Dupe the file into the file store
+ if( file_exists( $oldpath ) ) {
+ // Is our directory configured?
+ if( $store = FileStore::get( 'deleted' ) ) {
+ if( !$oimage->sha1 ) {
+ $oimage->upgradeRow(); // sha1 may be missing
+ }
+ $key = $oimage->sha1 . '.' . $oimage->getExtension();
+ $transaction->add( $store->insert( $key, $oldpath, FileStore::DELETE_ORIGINAL ) );
+ } else {
+ $group = null;
+ $key = null;
+ $transaction = false; // Return an error and do nothing
+ }
+ } else {
+ wfDebug( __METHOD__." deleting already-missing '$oldpath'; moving on to database\n" );
+ $group = null;
+ $key = '';
+ $transaction = new FSTransaction(); // empty
+ }
+
+ if( $transaction === false ) {
+ // Fail to restore?
+ wfDebug( __METHOD__.": import to file store failed, aborting\n" );
+ throw new MWException( "Could not archive and delete file $oldpath" );
+ return false;
+ }
+
+ wfDebug( __METHOD__.": set db items, applying file transactions\n" );
+ $transaction->commit();
+ FileStore::unlock();
+
+ $m = explode('!',$oimage->archive_name,2);
+ $timestamp = $m[0];
+
+ return $timestamp;
+ }
+
+ /**
+ * Moves an image from a safe private location
+ * Caller is responsible for clearing caches
+ * @param File $oimage
+ * @returns mixed, string timestamp on success, false on failure
+ */
+ function makeOldImagePublic( $oimage ) {
+ $transaction = new FSTransaction();
+ if( !FileStore::lock() ) {
+ wfDebug( __METHOD__." could not acquire filestore lock\n" );
+ return false;
+ }
+
+ $store = FileStore::get( 'deleted' );
+ if( !$store ) {
+ wfDebug( __METHOD__.": skipping row with no file.\n" );
+ return false;
+ }
+
+ $key = $oimage->sha1.'.'.$oimage->getExtension();
+ $destDir = $oimage->getArchivePath();
+ if( !is_dir( $destDir ) ) {
+ wfMkdirParents( $destDir );
+ }
+ $destPath = $destDir . DIRECTORY_SEPARATOR . $oimage->archive_name;
+ // Check if any other stored revisions use this file;
+ // if so, we shouldn't remove the file from the hidden
+ // archives so they will still work. Check hidden files first.
+ $useCount = $this->dbw->selectField( 'oldimage', '1',
+ array( 'oi_sha1' => $oimage->sha1,
+ 'oi_deleted & '.File::DELETED_FILE => File::DELETED_FILE ),
+ __METHOD__, array( 'FOR UPDATE' ) );
+ // Check the rest of the deleted archives too.
+ // (these are the ones that don't show in the image history)
+ if( !$useCount ) {
+ $useCount = $this->dbw->selectField( 'filearchive', '1',
+ array( 'fa_storage_group' => 'deleted', 'fa_storage_key' => $key ),
+ __METHOD__, array( 'FOR UPDATE' ) );
+ }
+
+ if( $useCount == 0 ) {
+ wfDebug( __METHOD__.": nothing else using {$oimage->sha1}, will deleting after\n" );
+ $flags = FileStore::DELETE_ORIGINAL;
+ } else {
+ $flags = 0;
+ }
+ $transaction->add( $store->export( $key, $destPath, $flags ) );
+
+ wfDebug( __METHOD__.": set db items, applying file transactions\n" );
+ $transaction->commit();
+ FileStore::unlock();
+
+ $m = explode('!',$oimage->archive_name,2);
+ $timestamp = $m[0];
+
+ return $timestamp;
+ }
+
+ /**
+ * Update the revision's rev_deleted field
+ * @param Revision $rev
+ * @param int $bitfield new rev_deleted bitfield value
+ */
+ function updateRevision( $rev, $bitfield ) {
+ $this->dbw->update( 'revision',
+ array( 'rev_deleted' => $bitfield ),
+ array( 'rev_id' => $rev->getId(),
+ 'rev_page' => $rev->getPage() ),
+ __METHOD__ );
+ }
+
+ /**
+ * Update the revision's rev_deleted field
+ * @param Revision $rev
+ * @param Title $title
+ * @param int $bitfield new rev_deleted bitfield value
+ */
+ function updateArchive( $rev, $title, $bitfield ) {
+ $this->dbw->update( 'archive',
+ array( 'ar_deleted' => $bitfield ),
+ array( 'ar_namespace' => $title->getNamespace(),
+ 'ar_title' => $title->getDBKey(),
+ 'ar_timestamp' => $this->dbw->timestamp( $rev->getTimestamp() ),
+ 'ar_rev_id' => $rev->getId() ),
+ __METHOD__ );
+ }
+
+ /**
+ * Update the images's oi_deleted field
+ * @param File $file
+ * @param int $bitfield new rev_deleted bitfield value
+ */
+ function updateOldFiles( $file, $bitfield ) {
+ $this->dbw->update( 'oldimage',
+ array( 'oi_deleted' => $bitfield ),
+ array( 'oi_name' => $file->getName(),
+ 'oi_timestamp' => $this->dbw->timestamp( $file->getTimestamp() ) ),
+ __METHOD__ );
+ }
+
+ /**
+ * Update the images's fa_deleted field
+ * @param ArchivedFile $file
+ * @param int $bitfield new rev_deleted bitfield value
+ */
+ function updateArchFiles( $file, $bitfield ) {
+ $this->dbw->update( 'filearchive',
+ array( 'fa_deleted' => $bitfield ),
+ array( 'fa_id' => $file->getId() ),
+ __METHOD__ );
+ }
+
+ /**
+ * Update the logging log_deleted field
+ * @param Row $row
+ * @param int $bitfield new rev_deleted bitfield value
+ */
+ function updateLogs( $row, $bitfield ) {
+ $this->dbw->update( 'logging',
+ array( 'log_deleted' => $bitfield ),
+ array( 'log_id' => $row->log_id ),
+ __METHOD__ );
+ }
+
+ /**
+ * Update the revision's recentchanges record if fields have been hidden
+ * @param Revision $rev
+ * @param int $bitfield new rev_deleted bitfield value
+ */
+ function updateRecentChangesEdits( $rev, $bitfield ) {
+ $this->dbw->update( 'recentchanges',
+ array( 'rc_deleted' => $bitfield,
+ 'rc_patrolled' => 1 ),
+ array( 'rc_this_oldid' => $rev->getId(),
+ 'rc_timestamp' => $this->dbw->timestamp( $rev->getTimestamp() ) ),
+ __METHOD__ );
+ }
+
+ /**
+ * Update the revision's recentchanges record if fields have been hidden
+ * @param Row $row
+ * @param int $bitfield new rev_deleted bitfield value
+ */
+ function updateRecentChangesLog( $row, $bitfield ) {
+ $this->dbw->update( 'recentchanges',
+ array( 'rc_deleted' => $bitfield,
+ 'rc_patrolled' => 1 ),
+ array( 'rc_logid' => $row->log_id,
+ 'rc_timestamp' => $row->log_timestamp ),
+ __METHOD__ );
+ }
+
+ /**
+ * Touch the page's cache invalidation timestamp; this forces cached
+ * history views to refresh, so any newly hidden or shown fields will
+ * update properly.
+ * @param Title $title
+ */
+ function updatePage( $title ) {
+ $title->invalidateCache();
+ $title->purgeSquid();
+
+ // Extensions that require referencing previous revisions may need this
+ wfRunHooks( 'ArticleRevisionVisiblitySet', array( &$title ) );
+ }
+
+ /**
+ * Checks for a change in the bitfield for a certain option and updates the
+ * provided array accordingly.
+ *
+ * @param String $desc Description to add to the array if the option was
+ * enabled / disabled.
+ * @param int $field The bitmask describing the single option.
+ * @param int $diff The xor of the old and new bitfields.
+ * @param array $arr The array to update.
+ */
+ function checkItem ( $desc, $field, $diff, $new, &$arr ) {
+ if ( $diff & $field ) {
+ $arr [ ( $new & $field ) ? 0 : 1 ][] = $desc;
+ }
+ }
+
+ /**
+ * Gets an array describing the changes made to the visibilit of the revision.
+ * If the resulting array is $arr, then $arr[0] will contain an array of strings
+ * describing the items that were hidden, $arr[2] will contain an array of strings
+ * describing the items that were unhidden, and $arr[3] will contain an array with
+ * a single string, which can be one of "applied restrictions to sysops",
+ * "removed restrictions from sysops", or null.
+ *
+ * @param int $n The new bitfield.
+ * @param int $o The old bitfield.
+ * @return An array as described above.
+ */
+ function getChanges ( $n, $o ) {
+ $diff = $n ^ $o;
+ $ret = array ( 0 => array(), 1 => array(), 2 => array() );
+
+ $this->checkItem ( wfMsgForContent ( 'revdelete-content' ),
+ Revision::DELETED_TEXT, $diff, $n, $ret );
+ $this->checkItem ( wfMsgForContent ( 'revdelete-summary' ),
+ Revision::DELETED_COMMENT, $diff, $n, $ret );
+ $this->checkItem ( wfMsgForContent ( 'revdelete-uname' ),
+ Revision::DELETED_USER, $diff, $n, $ret );
+
+ // Restriction application to sysops
+ if ( $diff & Revision::DELETED_RESTRICTED ) {
+ if ( $n & Revision::DELETED_RESTRICTED )
+ $ret[2][] = wfMsgForContent ( 'revdelete-restricted' );
+ else
+ $ret[2][] = wfMsgForContent ( 'revdelete-unrestricted' );
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Gets a log message to describe the given revision visibility change. This
+ * message will be of the form "[hid {content, edit summary, username}];
+ * [unhid {...}][applied restrictions to sysops] for $count revisions: $comment".
+ *
+ * @param int $count The number of effected revisions.
+ * @param int $nbitfield The new bitfield for the revision.
+ * @param int $obitfield The old bitfield for the revision.
+ * @param string $comment The comment associated with the change.
+ * @param bool $isForLog
+ */
+ function getLogMessage ( $count, $nbitfield, $obitfield, $comment, $isForLog = false ) {
+ global $wgContLang;
+
+ $s = '';
+ $changes = $this->getChanges( $nbitfield, $obitfield );
+
+ if ( count ( $changes[0] ) ) {
+ $s .= wfMsgForContent ( 'revdelete-hid', implode ( ', ', $changes[0] ) );
+ }
+
+ if ( count ( $changes[1] ) ) {
+ if ($s) $s .= '; ';
+
+ $s .= wfMsgForContent ( 'revdelete-unhid', implode ( ', ', $changes[1] ) );
+ }
+
+ if ( count ( $changes[2] )) {
+ if ($s)
+ $s .= ' (' . $changes[2][0] . ')';
+ else
+ $s = $changes[2][0];
+ }
+
+ $msg = $isForLog ? 'logdelete-log-message' : 'revdelete-log-message';
+ $ret = wfMsgExt ( $msg, array( 'parsemag', 'content' ),
+ $s, $wgContLang->formatNum( $count ) );
+
+ if ( $comment )
+ $ret .= ": $comment";
+
+ return $ret;
+
+ }
+
+ /**
+ * Record a log entry on the action
+ * @param Title $title, page where item was removed from
+ * @param int $count the number of revisions altered for this page
+ * @param int $nbitfield the new _deleted value
+ * @param int $obitfield the old _deleted value
+ * @param string $comment
+ * @param Title $target, the relevant page
+ * @param string $param, URL param
+ * @param Array $items
+ */
+ function updateLog( $title, $count, $nbitfield, $obitfield, $comment, $target, $param, $items = array() ) {
+ // Put things hidden from sysops in the oversight log
+ $logtype = ( ($nbitfield | $obitfield) & Revision::DELETED_RESTRICTED ) ? 'suppress' : 'delete';
+ $log = new LogPage( $logtype );
+
+ $reason = $this->getLogMessage ( $count, $nbitfield, $obitfield, $comment, $param == 'logid' );
+
+ if( $param == 'logid' ) {
+ $params = array( implode( ',', $items) );
+ $log->addEntry( 'event', $title, $reason, $params );
+ } else {
+ // Add params for effected page and ids
+ $params = array( $param, implode( ',', $items) );
+ $log->addEntry( 'revision', $title, $reason, $params );
+ }
+ }
+}
diff --git a/includes/specials/SpecialSearch.php b/includes/specials/SpecialSearch.php
new file mode 100644
index 00000000..f13c1676
--- /dev/null
+++ b/includes/specials/SpecialSearch.php
@@ -0,0 +1,651 @@
+<?php
+# Copyright (C) 2004 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
+
+/**
+ * Run text & title search and display the output
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Entry point
+ *
+ * @param $par String: (default '')
+ */
+function wfSpecialSearch( $par = '' ) {
+ global $wgRequest, $wgUser;
+
+ $search = str_replace( "\n", " ", $wgRequest->getText( 'search', $par ) );
+ $searchPage = new SpecialSearch( $wgRequest, $wgUser );
+ if( $wgRequest->getVal( 'fulltext' )
+ || !is_null( $wgRequest->getVal( 'offset' ))
+ || !is_null( $wgRequest->getVal( 'searchx' ))) {
+ $searchPage->showResults( $search, 'search' );
+ } else {
+ $searchPage->goResult( $search );
+ }
+}
+
+/**
+ * implements Special:Search - Run text & title search and display the output
+ * @ingroup SpecialPage
+ */
+class SpecialSearch {
+
+ /**
+ * 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 SpecialSearch( &$request, &$user ) {
+ list( $this->limit, $this->offset ) = $request->getLimitOffset( 20, 'searchlimit' );
+
+ $this->namespaces = $this->powerSearch( $request );
+ if( empty( $this->namespaces ) ) {
+ $this->namespaces = SearchEngine::userNamespaces( $user );
+ }
+
+ $this->searchRedirects = $request->getcheck( 'redirs' ) ? true : false;
+ }
+
+ /**
+ * If an exact title match can be found, jump straight ahead to it.
+ * @param string $term
+ * @public
+ */
+ function goResult( $term ) {
+ global $wgOut;
+ global $wgGoToEdit;
+
+ $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 ) ) {
+ wfRunHooks( 'SpecialSearchNogomatch', array( &$t ) );
+ # If the feature is enabled, go straight to the edit page
+ if ( $wgGoToEdit ) {
+ $wgOut->redirect( $t->getFullURL( 'action=edit' ) );
+ return;
+ }
+ }
+
+ $wgOut->wrapWikiMsg( "==$1==\n", 'notitlematches' );
+ if( $t->quickUserCan( 'create' ) && $t->quickUserCan( 'edit' ) ) {
+ $wgOut->addWikiMsg( 'noexactmatch', wfEscapeWikiText( $term ) );
+ } else {
+ $wgOut->addWikiMsg( 'noexactmatch-nocreate', wfEscapeWikiText( $term ) );
+ }
+
+ return $this->showResults( $term );
+ }
+
+ /**
+ * @param string $term
+ * @public
+ */
+ function showResults( $term ) {
+ $fname = 'SpecialSearch::showResults';
+ wfProfileIn( $fname );
+ global $wgOut, $wgUser;
+ $sk = $wgUser->getSkin();
+
+ $this->setupPage( $term );
+
+ $wgOut->addWikiMsg( 'searchresulttext' );
+
+ if( '' === trim( $term ) ) {
+ // Empty query -- straight view of search form
+ $wgOut->setSubtitle( '' );
+ $wgOut->addHTML( $this->powerSearchBox( $term ) );
+ $wgOut->addHTML( $this->powerSearchFocus() );
+ wfProfileOut( $fname );
+ return;
+ }
+
+ global $wgDisableTextSearch;
+ if ( $wgDisableTextSearch ) {
+ global $wgSearchForwardUrl;
+ if( $wgSearchForwardUrl ) {
+ $url = str_replace( '$1', urlencode( $term ), $wgSearchForwardUrl );
+ $wgOut->redirect( $url );
+ 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( $fname );
+ 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 );
+
+ // 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 );
+ 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);
+ $totalNum = 0;
+ if($titleMatches && !is_null($titleMatches->getTotalHits()))
+ $totalNum += $titleMatches->getTotalHits();
+ if($textMatches && !is_null($textMatches->getTotalHits()))
+ $totalNum += $textMatches->getTotalHits();
+ if ( $num > 0 ) {
+ if ( $totalNum > 0 ){
+ $top = wfMsgExt('showingresultstotal', array( 'parseinline' ),
+ $this->offset+1, $this->offset+$num, $totalNum );
+ } elseif ( $num >= $this->limit ) {
+ $top = wfShowingResults( $this->offset, $this->limit );
+ } else {
+ $top = wfShowingResultsNum( $this->offset, $this->limit, $num );
+ }
+ $wgOut->addHTML( "<p class='mw-search-numberresults'>{$top}</p>\n" );
+ }
+
+ // prev/next links
+ if( $num || $this->offset ) {
+ $prevnext = wfViewPrevNext( $this->offset, $this->limit,
+ SpecialPage::getTitleFor( 'Search' ),
+ wfArrayToCGI(
+ $this->powerSearchOptions(),
+ array( 'search' => $term ) ),
+ ($num < $this->limit) );
+ $wgOut->addHTML( "<p class='mw-search-pager-top'>{$prevnext}</p>\n" );
+ wfRunHooks( 'SpecialSearchResults', array( $term, &$titleMatches, &$textMatches ) );
+ } else {
+ wfRunHooks( 'SpecialSearchNoResults', array( $term ) );
+ }
+
+ if( $titleMatches ) {
+ if( $titleMatches->numRows() ) {
+ $wgOut->wrapWikiMsg( "==$1==\n", 'titlematches' );
+ $wgOut->addHTML( $this->showMatches( $titleMatches ) );
+ }
+ $titleMatches->free();
+ }
+
+ if( $textMatches ) {
+ // output appropriate heading
+ if( $textMatches->numRows() ) {
+ if($titleMatches)
+ $wgOut->wrapWikiMsg( "==$1==\n", 'textmatches' );
+ else // if no title matches the heading is redundant
+ $wgOut->addHTML("<hr/>");
+ } elseif( $num == 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( $textMatches->numRows() )
+ $wgOut->addHTML( $this->showMatches( $textMatches ) );
+
+ $textMatches->free();
+ }
+
+ if ( $num == 0 ) {
+ $wgOut->addWikiMsg( 'nonefound' );
+ }
+ if( $num || $this->offset ) {
+ $wgOut->addHTML( "<p class='mw-search-pager-bottom'>{$prevnext}</p>\n" );
+ }
+ $wgOut->addHTML( $this->powerSearchBox( $term ) );
+ wfProfileOut( $fname );
+ }
+
+ #------------------------------------------------------------------
+ # Private methods below this line
+
+ /**
+ *
+ */
+ function setupPage( $term ) {
+ global $wgOut;
+ if( !empty( $term ) )
+ $wgOut->setPageTitle( wfMsg( 'searchresults' ) );
+ $subtitlemsg = ( Title::newFromText( $term ) ? 'searchsubtitle' : 'searchsubtitleinvalid' );
+ $wgOut->setSubtitle( $wgOut->parse( wfMsg( $subtitlemsg, wfEscapeWikiText($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
+ * @private
+ */
+ 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
+ * @private
+ */
+ function powerSearchOptions() {
+ $opt = array();
+ foreach( $this->namespaces as $n ) {
+ $opt['ns' . $n] = 1;
+ }
+ $opt['redirs'] = $this->searchRedirects ? 1 : 0;
+ return $opt;
+ }
+
+ /**
+ * Show whole set of results
+ *
+ * @param SearchResultSet $matches
+ */
+ function showMatches( &$matches ) {
+ $fname = 'SpecialSearch::showMatches';
+ wfProfileIn( $fname );
+
+ global $wgContLang;
+ $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
+ global $wgContLang;
+ $out = $wgContLang->convert( $out );
+ wfProfileOut( $fname );
+ return $out;
+ }
+
+ /**
+ * Format a single hit result
+ * @param SearchResult $result
+ * @param array $terms terms to highlight
+ */
+ function showHit( $result, $terms ) {
+ $fname = 'SpecialSearch::showHit';
+ wfProfileIn( $fname );
+ global $wgUser, $wgContLang, $wgLang;
+
+ if( $result->isBrokenTitle() ) {
+ wfProfileOut( $fname );
+ return "<!-- Broken link in search result -->\n";
+ }
+
+ $t = $result->getTitle();
+ $sk = $wgUser->getSkin();
+
+ $link = $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( $fname );
+ 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( $fname );
+ 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',$sk->makeKnownLinkObj( $redirectTitle, $redirectText))
+ ."</span>";
+ $section = '';
+ if( !is_null($sectionTitle) )
+ $section = "<span class='searchalttitle'>"
+ .wfMsg('search-section', $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' ),
+ $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 = ' -- <a href="'.$st->escapeLocalURL($stParams).'">'.
+ wfMsg('search-relatedarticle').'</a>';
+ }
+
+ // Include a thumbnail for media files...
+ if( $t->getNamespace() == NS_IMAGE ) {
+ $img = wfFindFile( $t );
+ if( $img ) {
+ $thumb = $img->getThumbnail( 120, 120 );
+ if( $thumb ) {
+ $desc = $img->getShortDesc();
+ wfProfileOut( $fname );
+ // Ugly table. :D
+ // Float doesn't seem to interact well with the bullets.
+ // Table messes up vertical alignment of the bullet, but I'm
+ // not sure what more I can do about that. :(
+ return "<li>" .
+ '<table class="searchResultImage">' .
+ '<tr>' .
+ '<td width="120" align="center">' .
+ $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( $fname );
+ 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
+ */
+ function showInterwiki( &$matches, $query ) {
+ $fname = 'SpecialSearch::showInterwiki';
+ wfProfileIn( $fname );
+
+ global $wgContLang;
+ $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 start='{$off}' 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
+ global $wgContLang;
+ $out = $wgContLang->convert( $out );
+ wfProfileOut( $fname );
+ return $out;
+ }
+
+ /**
+ * Show single interwiki link
+ *
+ * @param SearchResult $result
+ * @param string $lastInterwiki
+ * @param array $terms
+ * @param string $query
+ * @param array $customCaptions iw prefix -> caption
+ */
+ function showInterwikiHit( $result, $lastInterwiki, $terms, $query, $customCaptions){
+ $fname = 'SpecialSearch::showInterwikiHit';
+ wfProfileIn( $fname );
+ global $wgUser, $wgContLang, $wgLang;
+
+ if( $result->isBrokenTitle() ) {
+ wfProfileOut( $fname );
+ return "<!-- Broken link in search result -->\n";
+ }
+
+ $t = $result->getTitle();
+ $sk = $wgUser->getSkin();
+
+ $link = $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',$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 = $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( $fname );
+ return $out;
+ }
+
+
+ /**
+ * Generates the power search box at bottom of [[Special:Search]]
+ * @param $term string: search term
+ * @return $out string: HTML form
+ */
+ function powerSearchBox( $term ) {
+ global $wgScript;
+
+ $namespaces = '';
+ foreach( SearchEngine::searchableNamespaces() as $ns => $name ) {
+ $name = str_replace( '_', ' ', $name );
+ if( '' == $name ) {
+ $name = wfMsg( 'blanknamespace' );
+ }
+ $namespaces .= Xml::openElement( 'span', array( 'style' => 'white-space: nowrap' ) ) .
+ Xml::checkLabel( $name, "ns{$ns}", "mw-search-ns{$ns}", in_array( $ns, $this->namespaces ) ) .
+ Xml::closeElement( 'span' ) . "\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";
+
+ $out = Xml::openElement( 'form', array( 'id' => 'powersearch', 'method' => 'get', 'action' => $wgScript ) ) .
+ Xml::fieldset( wfMsg( 'powersearch-legend' ),
+ Xml::hidden( 'title', 'Special:Search' ) .
+ "<p>" .
+ wfMsgExt( 'powersearch-ns', array( 'parseinline' ) ) .
+ "<br />" .
+ $namespaces .
+ "</p>" .
+ "<p>" .
+ $redirect . " " . $redirectLabel .
+ "</p>" .
+ wfMsgExt( 'powersearch-field', array( 'parseinline' ) ) .
+ "&nbsp;" .
+ $searchField .
+ "&nbsp;" .
+ $searchButton ) .
+ "</form>";
+
+ return $out;
+ }
+
+ function powerSearchFocus() {
+ global $wgJsMimeType;
+ return "<script type=\"$wgJsMimeType\">" .
+ "hookEvent(\"load\", function(){" .
+ "document.getElementById('powerSearchText').focus();" .
+ "});" .
+ "</script>";
+ }
+
+ function shortDialog($term) {
+ global $wgScript;
+
+ $out = Xml::openElement( 'form', array(
+ 'id' => 'search',
+ 'method' => 'get',
+ 'action' => $wgScript
+ ));
+ $out .= Xml::hidden( 'title', 'Special:Search' );
+ $out .= Xml::input( 'search', 50, $term, array( 'type' => 'text', 'id' => 'searchText' ) ) . ' ';
+ foreach( SearchEngine::searchableNamespaces() as $ns => $name ) {
+ if( in_array( $ns, $this->namespaces ) ) {
+ $out .= Xml::hidden( "ns{$ns}", '1' );
+ }
+ }
+ $out .= Xml::submitButton( wfMsg( 'searchbutton' ), array( 'name' => 'fulltext' ) );
+ $out .= Xml::closeElement( 'form' );
+
+ return $out;
+ }
+}
diff --git a/includes/specials/SpecialShortpages.php b/includes/specials/SpecialShortpages.php
new file mode 100644
index 00000000..2e7d24a5
--- /dev/null
+++ b/includes/specials/SpecialShortpages.php
@@ -0,0 +1,98 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * SpecialShortpages extends QueryPage. It is used to return the shortest
+ * pages in the database.
+ * @ingroup SpecialPage
+ */
+class ShortPagesPage extends QueryPage {
+
+ function getName() {
+ return 'Shortpages';
+ }
+
+ /**
+ * This query is indexed as of 1.5
+ */
+ function isExpensive() {
+ return true;
+ }
+
+ function isSyndicated() {
+ return false;
+ }
+
+ function getSQL() {
+ global $wgContentNamespaces;
+
+ $dbr = wfGetDB( DB_SLAVE );
+ $page = $dbr->tableName( 'page' );
+ $name = $dbr->addQuotes( $this->getName() );
+
+ $forceindex = $dbr->useIndexClause("page_len");
+
+ if ($wgContentNamespaces)
+ $nsclause = "page_namespace IN (" . $dbr->makeList($wgContentNamespaces) . ")";
+ else
+ $nsclause = "page_namespace = " . NS_MAIN;
+
+ return
+ "SELECT $name as type,
+ page_namespace as namespace,
+ page_title as title,
+ page_len AS value
+ FROM $page $forceindex
+ WHERE $nsclause AND page_is_redirect=0";
+ }
+
+ function preprocessResults( $db, $res ) {
+ # There's no point doing a batch check if we aren't caching results;
+ # the page must exist for it to have been pulled out of the table
+ if( $this->isCached() ) {
+ $batch = new LinkBatch();
+ while( $row = $db->fetchObject( $res ) )
+ $batch->add( $row->namespace, $row->title );
+ $batch->execute();
+ if( $db->numRows( $res ) > 0 )
+ $db->dataSeek( $res, 0 );
+ }
+ }
+
+ function sortDescending() {
+ return false;
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgLang, $wgContLang;
+ $dm = $wgContLang->getDirMark();
+
+ $title = Title::makeTitleSafe( $result->namespace, $result->title );
+ if ( !$title ) {
+ return '<!-- Invalid title ' . htmlspecialchars( "{$result->namespace}:{$result->title}" ). '-->';
+ }
+ $hlink = $skin->makeKnownLinkObj( $title, wfMsgHtml( 'hist' ), 'action=history' );
+ $plink = $this->isCached()
+ ? $skin->makeLinkObj( $title )
+ : $skin->makeKnownLinkObj( $title );
+ $size = wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ), $wgLang->formatNum( htmlspecialchars( $result->value ) ) );
+
+ return $title->exists()
+ ? "({$hlink}) {$dm}{$plink} {$dm}[{$size}]"
+ : "<s>({$hlink}) {$dm}{$plink} {$dm}[{$size}]</s>";
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialShortpages() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $spp = new ShortPagesPage();
+
+ return $spp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialSpecialpages.php b/includes/specials/SpecialSpecialpages.php
new file mode 100644
index 00000000..ca91ad51
--- /dev/null
+++ b/includes/specials/SpecialSpecialpages.php
@@ -0,0 +1,82 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ *
+ */
+function wfSpecialSpecialpages() {
+ global $wgOut, $wgUser, $wgMessageCache, $wgSortSpecialPages;
+
+ $wgMessageCache->loadAllMessages();
+
+ $wgOut->setRobotpolicy( 'noindex,nofollow' ); # Is this really needed?
+ $sk = $wgUser->getSkin();
+
+ $pages = SpecialPage::getUsablePages();
+
+ if( count( $pages ) == 0 ) {
+ # Yeah, that was pointless. Thanks for coming.
+ return;
+ }
+
+ /** Put them into a sortable array */
+ $groups = array();
+ foreach ( $pages as $page ) {
+ if ( $page->isListed() ) {
+ $group = SpecialPage::getGroup( $page );
+ if( !isset($groups[$group]) ) {
+ $groups[$group] = array();
+ }
+ $groups[$group][$page->getDescription()] = array( $page->getTitle(), $page->isRestricted() );
+ }
+ }
+
+ /** Sort */
+ if ( $wgSortSpecialPages ) {
+ foreach( $groups as $group => $sortedPages ) {
+ ksort( $groups[$group] );
+ }
+ }
+
+ /** Always move "other" to end */
+ if( array_key_exists('other',$groups) ) {
+ $other = $groups['other'];
+ unset( $groups['other'] );
+ $groups['other'] = $other;
+ }
+
+ /** Now output the HTML */
+ foreach ( $groups as $group => $sortedPages ) {
+ $middle = ceil( count($sortedPages)/2 );
+ $total = count($sortedPages);
+ $count = 0;
+
+ $wgOut->addHTML( "<h4 class='mw-specialpagesgroup'>".wfMsgHtml("specialpages-group-$group")."</h4>\n" );
+ $wgOut->addHTML( "<table style='width: 100%;' class='mw-specialpages-table'><tr>" );
+ $wgOut->addHTML( "<td width='30%' valign='top'><ul>\n" );
+ foreach( $sortedPages as $desc => $specialpage ) {
+ list( $title, $restricted ) = $specialpage;
+ $link = $sk->makeKnownLinkObj( $title , htmlspecialchars( $desc ) );
+ if( $restricted ) {
+ $wgOut->addHTML( "<li class='mw-specialpages-page mw-specialpagerestricted'>{$link}</li>\n" );
+ } else {
+ $wgOut->addHTML( "<li>{$link}</li>\n" );
+ }
+
+ # Split up the larger groups
+ $count++;
+ if( $total > 3 && $count == $middle ) {
+ $wgOut->addHTML( "</ul></td><td width='10%'></td><td width='30%' valign='top'><ul>" );
+ }
+ }
+ $wgOut->addHTML( "</ul></td><td width='30%' valign='top'></td></tr></table>\n" );
+ }
+ $wgOut->addHTML(
+ Xml::openElement('div', array( 'class' => 'mw-specialpages-notes' )).
+ wfMsgWikiHtml('specialpages-note').
+ Xml::closeElement('div')
+ );
+}
diff --git a/includes/specials/SpecialStatistics.php b/includes/specials/SpecialStatistics.php
new file mode 100644
index 00000000..570a21c6
--- /dev/null
+++ b/includes/specials/SpecialStatistics.php
@@ -0,0 +1,93 @@
+<?php
+
+/**
+ * Special page lists various statistics, including the contents of
+ * `site_stats`, plus page view details if enabled
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Show the special page
+ *
+ * @param mixed $par (not used)
+ */
+function wfSpecialStatistics( $par = '' ) {
+ global $wgOut, $wgLang, $wgRequest;
+ $dbr = wfGetDB( DB_SLAVE );
+
+ $views = SiteStats::views();
+ $edits = SiteStats::edits();
+ $good = SiteStats::articles();
+ $images = SiteStats::images();
+ $total = SiteStats::pages();
+ $users = SiteStats::users();
+ $admins = SiteStats::admins();
+ $numJobs = SiteStats::jobs();
+
+ 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";
+
+ $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";
+
+ global $wgDisableCounters, $wgMiserMode, $wgUser, $wgLang, $wgContLang;
+ if( !$wgDisableCounters && !$wgMiserMode ) {
+ $res = $dbr->select(
+ 'page',
+ array(
+ 'page_namespace',
+ 'page_title',
+ 'page_counter',
+ ),
+ array(
+ 'page_is_redirect' => 0,
+ 'page_counter > 0',
+ ),
+ __METHOD__,
+ array(
+ 'ORDER BY' => 'page_counter DESC',
+ 'LIMIT' => 10,
+ )
+ );
+ if( $res->numRows() > 0 ) {
+ $text .= "==" . wfMsgNoTrans( 'statistics-mostpopular' ) . "==\n";
+ while( $row = $res->fetchObject() ) {
+ $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title );
+ if( $title instanceof Title )
+ $text .= '* [[:' . $title->getPrefixedText() . ']] (' . $wgLang->formatNum( $row->page_counter ) . ")\n";
+ }
+ $res->free();
+ }
+ }
+
+ $footer = wfMsgNoTrans( 'statistics-footer' );
+ if( !wfEmptyMsg( 'statistics-footer', $footer ) && $footer != '' )
+ $text .= "\n" . $footer;
+
+ $wgOut->addWikiText( $text );
+ }
+}
diff --git a/includes/specials/SpecialUncategorizedcategories.php b/includes/specials/SpecialUncategorizedcategories.php
new file mode 100644
index 00000000..f23e89ce
--- /dev/null
+++ b/includes/specials/SpecialUncategorizedcategories.php
@@ -0,0 +1,30 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * implements Special:Uncategorizedcategories
+ * @ingroup SpecialPage
+ */
+class UncategorizedCategoriesPage extends UncategorizedPagesPage {
+ function UncategorizedCategoriesPage() {
+ $this->requestedNamespace = NS_CATEGORY;
+ }
+
+ function getName() {
+ return "Uncategorizedcategories";
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialUncategorizedcategories() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $lpp = new UncategorizedCategoriesPage();
+
+ return $lpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialUncategorizedimages.php b/includes/specials/SpecialUncategorizedimages.php
new file mode 100644
index 00000000..986ec967
--- /dev/null
+++ b/includes/specials/SpecialUncategorizedimages.php
@@ -0,0 +1,48 @@
+<?php
+/**
+ * Special page lists images which haven't been categorised
+ *
+ * @file
+ * @ingroup SpecialPage
+ * @author Rob Church <robchur@gmail.com>
+ */
+
+/**
+ * @ingroup SpecialPage
+ */
+class UncategorizedImagesPage extends ImageQueryPage {
+
+ function getName() {
+ return 'Uncategorizedimages';
+ }
+
+ function sortDescending() {
+ return false;
+ }
+
+ function isExpensive() {
+ return true;
+ }
+
+ function isSyndicated() {
+ return false;
+ }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ list( $page, $categorylinks ) = $dbr->tableNamesN( 'page', 'categorylinks' );
+ $ns = NS_IMAGE;
+
+ return "SELECT 'Uncategorizedimages' AS type, page_namespace AS namespace,
+ page_title AS title, page_title AS value
+ FROM {$page} LEFT JOIN {$categorylinks} ON page_id = cl_from
+ WHERE cl_from IS NULL AND page_namespace = {$ns} AND page_is_redirect = 0";
+ }
+
+}
+
+function wfSpecialUncategorizedimages() {
+ $uip = new UncategorizedImagesPage();
+ list( $limit, $offset ) = wfCheckLimits();
+ return $uip->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialUncategorizedpages.php b/includes/specials/SpecialUncategorizedpages.php
new file mode 100644
index 00000000..e7f0aaca
--- /dev/null
+++ b/includes/specials/SpecialUncategorizedpages.php
@@ -0,0 +1,55 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * A special page looking for page without any category.
+ * @ingroup SpecialPage
+ */
+class UncategorizedPagesPage extends PageQueryPage {
+ var $requestedNamespace = NS_MAIN;
+
+ function getName() {
+ return "Uncategorizedpages";
+ }
+
+ function sortDescending() {
+ return false;
+ }
+
+ function isExpensive() {
+ return true;
+ }
+ function isSyndicated() { return false; }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ list( $page, $categorylinks ) = $dbr->tableNamesN( 'page', 'categorylinks' );
+ $name = $dbr->addQuotes( $this->getName() );
+
+ return
+ "
+ SELECT
+ $name as type,
+ page_namespace AS namespace,
+ page_title AS title,
+ page_title AS value
+ FROM $page
+ LEFT JOIN $categorylinks ON page_id=cl_from
+ WHERE cl_from IS NULL AND page_namespace={$this->requestedNamespace} AND page_is_redirect=0
+ ";
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialUncategorizedpages() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $lpp = new UncategorizedPagesPage();
+
+ return $lpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialUncategorizedtemplates.php b/includes/specials/SpecialUncategorizedtemplates.php
new file mode 100644
index 00000000..cb2a6d40
--- /dev/null
+++ b/includes/specials/SpecialUncategorizedtemplates.php
@@ -0,0 +1,33 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Special page lists all uncategorised pages in the
+ * template namespace
+ *
+ * @ingroup SpecialPage
+ * @author Rob Church <robchur@gmail.com>
+ */
+class UncategorizedTemplatesPage extends UncategorizedPagesPage {
+
+ var $requestedNamespace = NS_TEMPLATE;
+
+ public function getName() {
+ return 'Uncategorizedtemplates';
+ }
+
+}
+
+/**
+ * Main execution point
+ *
+ * @param mixed $par Parameter passed to the page
+ */
+function wfSpecialUncategorizedtemplates() {
+ list( $limit, $offset ) = wfCheckLimits();
+ $utp = new UncategorizedTemplatesPage();
+ $utp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialUndelete.php b/includes/specials/SpecialUndelete.php
new file mode 100644
index 00000000..fbbf89d6
--- /dev/null
+++ b/includes/specials/SpecialUndelete.php
@@ -0,0 +1,1276 @@
+<?php
+
+/**
+ * Special page allowing users with the appropriate permissions to view
+ * and restore deleted content
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Constructor
+ */
+function wfSpecialUndelete( $par ) {
+ global $wgRequest;
+
+ $form = new UndeleteForm( $wgRequest, $par );
+ $form->execute();
+}
+
+/**
+ * Used to show archived pages and eventually restore them.
+ * @ingroup SpecialPage
+ */
+class PageArchive {
+ protected $title;
+ var $fileStatus;
+
+ function __construct( $title ) {
+ if( is_null( $title ) ) {
+ throw new MWException( 'Archiver() given a null title.');
+ }
+ $this->title = $title;
+ }
+
+ /**
+ * List all deleted pages recorded in the archive table. Returns result
+ * wrapper with (ar_namespace, ar_title, count) fields, ordered by page
+ * namespace/title.
+ *
+ * @return ResultWrapper
+ */
+ public static function listAllPages() {
+ $dbr = wfGetDB( DB_SLAVE );
+ return self::listPages( $dbr, '' );
+ }
+
+ /**
+ * List deleted pages recorded in the archive table matching the
+ * given title prefix.
+ * Returns result wrapper with (ar_namespace, ar_title, count) fields.
+ *
+ * @return ResultWrapper
+ */
+ public static function listPagesByPrefix( $prefix ) {
+ $dbr = wfGetDB( DB_SLAVE );
+
+ $title = Title::newFromText( $prefix );
+ if( $title ) {
+ $ns = $title->getNamespace();
+ $encPrefix = $dbr->escapeLike( $title->getDBkey() );
+ } else {
+ // Prolly won't work too good
+ // @todo handle bare namespace names cleanly?
+ $ns = 0;
+ $encPrefix = $dbr->escapeLike( $prefix );
+ }
+ $conds = array(
+ 'ar_namespace' => $ns,
+ "ar_title LIKE '$encPrefix%'",
+ );
+ return self::listPages( $dbr, $conds );
+ }
+
+ protected static function listPages( $dbr, $condition ) {
+ return $dbr->resultObject(
+ $dbr->select(
+ array( 'archive' ),
+ array(
+ 'ar_namespace',
+ 'ar_title',
+ 'COUNT(*) AS count'
+ ),
+ $condition,
+ __METHOD__,
+ array(
+ 'GROUP BY' => 'ar_namespace,ar_title',
+ 'ORDER BY' => 'ar_namespace,ar_title',
+ 'LIMIT' => 100,
+ )
+ )
+ );
+ }
+
+ /**
+ * List the revisions of the given page. Returns result wrapper with
+ * (ar_minor_edit, ar_timestamp, ar_user, ar_user_text, ar_comment) fields.
+ *
+ * @return ResultWrapper
+ */
+ function listRevisions() {
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select( 'archive',
+ array( 'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text', 'ar_comment', 'ar_len', 'ar_deleted' ),
+ array( 'ar_namespace' => $this->title->getNamespace(),
+ 'ar_title' => $this->title->getDBkey() ),
+ 'PageArchive::listRevisions',
+ array( 'ORDER BY' => 'ar_timestamp DESC' ) );
+ $ret = $dbr->resultObject( $res );
+ return $ret;
+ }
+
+ /**
+ * List the deleted file revisions for this page, if it's a file page.
+ * Returns a result wrapper with various filearchive fields, or null
+ * if not a file page.
+ *
+ * @return ResultWrapper
+ * @todo Does this belong in Image for fuller encapsulation?
+ */
+ function listFiles() {
+ if( $this->title->getNamespace() == NS_IMAGE ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select( 'filearchive',
+ array(
+ 'fa_id',
+ 'fa_name',
+ 'fa_archive_name',
+ 'fa_storage_key',
+ 'fa_storage_group',
+ 'fa_size',
+ 'fa_width',
+ 'fa_height',
+ 'fa_bits',
+ 'fa_metadata',
+ 'fa_media_type',
+ 'fa_major_mime',
+ 'fa_minor_mime',
+ 'fa_description',
+ 'fa_user',
+ 'fa_user_text',
+ 'fa_timestamp',
+ 'fa_deleted' ),
+ array( 'fa_name' => $this->title->getDBkey() ),
+ __METHOD__,
+ array( 'ORDER BY' => 'fa_timestamp DESC' ) );
+ $ret = $dbr->resultObject( $res );
+ return $ret;
+ }
+ return null;
+ }
+
+ /**
+ * Fetch (and decompress if necessary) the stored text for the deleted
+ * revision of the page with the given timestamp.
+ *
+ * @return string
+ * @deprecated Use getRevision() for more flexible information
+ */
+ function getRevisionText( $timestamp ) {
+ $rev = $this->getRevision( $timestamp );
+ return $rev ? $rev->getText() : null;
+ }
+
+ /**
+ * Return a Revision object containing data for the deleted revision.
+ * Note that the result *may* or *may not* have a null page ID.
+ * @param string $timestamp
+ * @return Revision
+ */
+ function getRevision( $timestamp ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ $row = $dbr->selectRow( 'archive',
+ array(
+ 'ar_rev_id',
+ 'ar_text',
+ 'ar_comment',
+ 'ar_user',
+ 'ar_user_text',
+ 'ar_timestamp',
+ 'ar_minor_edit',
+ 'ar_flags',
+ 'ar_text_id',
+ 'ar_deleted',
+ 'ar_len' ),
+ array( 'ar_namespace' => $this->title->getNamespace(),
+ 'ar_title' => $this->title->getDBkey(),
+ 'ar_timestamp' => $dbr->timestamp( $timestamp ) ),
+ __METHOD__ );
+ if( $row ) {
+ return new Revision( array(
+ 'page' => $this->title->getArticleId(),
+ 'id' => $row->ar_rev_id,
+ 'text' => ($row->ar_text_id
+ ? null
+ : Revision::getRevisionText( $row, 'ar_' ) ),
+ 'comment' => $row->ar_comment,
+ 'user' => $row->ar_user,
+ 'user_text' => $row->ar_user_text,
+ 'timestamp' => $row->ar_timestamp,
+ 'minor_edit' => $row->ar_minor_edit,
+ 'text_id' => $row->ar_text_id,
+ 'deleted' => $row->ar_deleted,
+ 'len' => $row->ar_len) );
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Return the most-previous revision, either live or deleted, against
+ * the deleted revision given by timestamp.
+ *
+ * May produce unexpected results in case of history merges or other
+ * unusual time issues.
+ *
+ * @param string $timestamp
+ * @return Revision or null
+ */
+ function getPreviousRevision( $timestamp ) {
+ $dbr = wfGetDB( DB_SLAVE );
+
+ // Check the previous deleted revision...
+ $row = $dbr->selectRow( 'archive',
+ 'ar_timestamp',
+ array( 'ar_namespace' => $this->title->getNamespace(),
+ 'ar_title' => $this->title->getDBkey(),
+ 'ar_timestamp < ' .
+ $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ),
+ __METHOD__,
+ array(
+ 'ORDER BY' => 'ar_timestamp DESC',
+ 'LIMIT' => 1 ) );
+ $prevDeleted = $row ? wfTimestamp( TS_MW, $row->ar_timestamp ) : false;
+
+ $row = $dbr->selectRow( array( 'page', 'revision' ),
+ array( 'rev_id', 'rev_timestamp' ),
+ array(
+ 'page_namespace' => $this->title->getNamespace(),
+ 'page_title' => $this->title->getDBkey(),
+ 'page_id = rev_page',
+ 'rev_timestamp < ' .
+ $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ),
+ __METHOD__,
+ array(
+ 'ORDER BY' => 'rev_timestamp DESC',
+ 'LIMIT' => 1 ) );
+ $prevLive = $row ? wfTimestamp( TS_MW, $row->rev_timestamp ) : false;
+ $prevLiveId = $row ? intval( $row->rev_id ) : null;
+
+ if( $prevLive && $prevLive > $prevDeleted ) {
+ // Most prior revision was live
+ return Revision::newFromId( $prevLiveId );
+ } elseif( $prevDeleted ) {
+ // Most prior revision was deleted
+ return $this->getRevision( $prevDeleted );
+ } else {
+ // No prior revision on this page.
+ return null;
+ }
+ }
+
+ /**
+ * Get the text from an archive row containing ar_text, ar_flags and ar_text_id
+ */
+ function getTextFromRow( $row ) {
+ if( is_null( $row->ar_text_id ) ) {
+ // An old row from MediaWiki 1.4 or previous.
+ // Text is embedded in this row in classic compression format.
+ return Revision::getRevisionText( $row, "ar_" );
+ } else {
+ // New-style: keyed to the text storage backend.
+ $dbr = wfGetDB( DB_SLAVE );
+ $text = $dbr->selectRow( 'text',
+ array( 'old_text', 'old_flags' ),
+ array( 'old_id' => $row->ar_text_id ),
+ __METHOD__ );
+ return Revision::getRevisionText( $text );
+ }
+ }
+
+
+ /**
+ * Fetch (and decompress if necessary) the stored text of the most
+ * recently edited deleted revision of the page.
+ *
+ * If there are no archived revisions for the page, returns NULL.
+ *
+ * @return string
+ */
+ function getLastRevisionText() {
+ $dbr = wfGetDB( DB_SLAVE );
+ $row = $dbr->selectRow( 'archive',
+ array( 'ar_text', 'ar_flags', 'ar_text_id' ),
+ array( 'ar_namespace' => $this->title->getNamespace(),
+ 'ar_title' => $this->title->getDBkey() ),
+ 'PageArchive::getLastRevisionText',
+ array( 'ORDER BY' => 'ar_timestamp DESC' ) );
+ if( $row ) {
+ return $this->getTextFromRow( $row );
+ } else {
+ return NULL;
+ }
+ }
+
+ /**
+ * Quick check if any archived revisions are present for the page.
+ * @return bool
+ */
+ function isDeleted() {
+ $dbr = wfGetDB( DB_SLAVE );
+ $n = $dbr->selectField( 'archive', 'COUNT(ar_title)',
+ array( 'ar_namespace' => $this->title->getNamespace(),
+ 'ar_title' => $this->title->getDBkey() ) );
+ return ($n > 0);
+ }
+
+ /**
+ * Restore the given (or all) text and file revisions for the page.
+ * Once restored, the items will be removed from the archive tables.
+ * The deletion log will be updated with an undeletion notice.
+ *
+ * @param array $timestamps Pass an empty array to restore all revisions, otherwise list the ones to undelete.
+ * @param string $comment
+ * @param array $fileVersions
+ * @param bool $unsuppress
+ *
+ * @return array(number of file revisions restored, number of image revisions restored, log message)
+ * on success, false on failure
+ */
+ function undelete( $timestamps, $comment = '', $fileVersions = array(), $unsuppress = false ) {
+ // If both the set of text revisions and file revisions are empty,
+ // restore everything. Otherwise, just restore the requested items.
+ $restoreAll = empty( $timestamps ) && empty( $fileVersions );
+
+ $restoreText = $restoreAll || !empty( $timestamps );
+ $restoreFiles = $restoreAll || !empty( $fileVersions );
+
+ if( $restoreFiles && $this->title->getNamespace() == NS_IMAGE ) {
+ $img = wfLocalFile( $this->title );
+ $this->fileStatus = $img->restore( $fileVersions, $unsuppress );
+ $filesRestored = $this->fileStatus->successCount;
+ } else {
+ $filesRestored = 0;
+ }
+
+ if( $restoreText ) {
+ $textRestored = $this->undeleteRevisions( $timestamps, $unsuppress );
+ if($textRestored === false) // It must be one of UNDELETE_*
+ return false;
+ } else {
+ $textRestored = 0;
+ }
+
+ // Touch the log!
+ global $wgContLang;
+ $log = new LogPage( 'delete' );
+
+ if( $textRestored && $filesRestored ) {
+ $reason = wfMsgExt( 'undeletedrevisions-files', array( 'content', 'parsemag' ),
+ $wgContLang->formatNum( $textRestored ),
+ $wgContLang->formatNum( $filesRestored ) );
+ } elseif( $textRestored ) {
+ $reason = wfMsgExt( 'undeletedrevisions', array( 'content', 'parsemag' ),
+ $wgContLang->formatNum( $textRestored ) );
+ } elseif( $filesRestored ) {
+ $reason = wfMsgExt( 'undeletedfiles', array( 'content', 'parsemag' ),
+ $wgContLang->formatNum( $filesRestored ) );
+ } else {
+ wfDebug( "Undelete: nothing undeleted...\n" );
+ return false;
+ }
+
+ if( trim( $comment ) != '' )
+ $reason .= ": {$comment}";
+ $log->addEntry( 'restore', $this->title, $reason );
+
+ return array($textRestored, $filesRestored, $reason);
+ }
+
+ /**
+ * This is the meaty bit -- restores archived revisions of the given page
+ * to the cur/old tables. If the page currently exists, all revisions will
+ * be stuffed into old, otherwise the most recent will go into cur.
+ *
+ * @param array $timestamps Pass an empty array to restore all revisions, otherwise list the ones to undelete.
+ * @param string $comment
+ * @param array $fileVersions
+ * @param bool $unsuppress, remove all ar_deleted/fa_deleted restrictions of seletected revs
+ *
+ * @return mixed number of revisions restored or false on failure
+ */
+ private function undeleteRevisions( $timestamps, $unsuppress = false ) {
+ if ( wfReadOnly() )
+ return false;
+ $restoreAll = empty( $timestamps );
+
+ $dbw = wfGetDB( DB_MASTER );
+
+ # Does this page already exist? We'll have to update it...
+ $article = new Article( $this->title );
+ $options = 'FOR UPDATE';
+ $page = $dbw->selectRow( 'page',
+ array( 'page_id', 'page_latest' ),
+ array( 'page_namespace' => $this->title->getNamespace(),
+ 'page_title' => $this->title->getDBkey() ),
+ __METHOD__,
+ $options );
+ if( $page ) {
+ $makepage = false;
+ # Page already exists. Import the history, and if necessary
+ # we'll update the latest revision field in the record.
+ $newid = 0;
+ $pageId = $page->page_id;
+ $previousRevId = $page->page_latest;
+ # Get the time span of this page
+ $previousTimestamp = $dbw->selectField( 'revision', 'rev_timestamp',
+ array( 'rev_id' => $previousRevId ),
+ __METHOD__ );
+ if( $previousTimestamp === false ) {
+ wfDebug( __METHOD__.": existing page refers to a page_latest that does not exist\n" );
+ return 0;
+ }
+ } else {
+ # Have to create a new article...
+ $makepage = true;
+ $previousRevId = 0;
+ $previousTimestamp = 0;
+ }
+
+ if( $restoreAll ) {
+ $oldones = '1 = 1'; # All revisions...
+ } else {
+ $oldts = implode( ',',
+ array_map( array( &$dbw, 'addQuotes' ),
+ array_map( array( &$dbw, 'timestamp' ),
+ $timestamps ) ) );
+
+ $oldones = "ar_timestamp IN ( {$oldts} )";
+ }
+
+ /**
+ * Select each archived revision...
+ */
+ $result = $dbw->select( 'archive',
+ /* fields */ array(
+ 'ar_rev_id',
+ 'ar_text',
+ 'ar_comment',
+ 'ar_user',
+ 'ar_user_text',
+ 'ar_timestamp',
+ 'ar_minor_edit',
+ 'ar_flags',
+ 'ar_text_id',
+ 'ar_deleted',
+ 'ar_page_id',
+ 'ar_len' ),
+ /* WHERE */ array(
+ 'ar_namespace' => $this->title->getNamespace(),
+ 'ar_title' => $this->title->getDBkey(),
+ $oldones ),
+ __METHOD__,
+ /* 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 );
+ $pageId = $newid;
+ }
+
+ $revision = null;
+ $restored = 0;
+
+ while( $row = $ret->fetchObject() ) {
+ if( $row->ar_text_id ) {
+ // Revision was deleted in 1.5+; text is in
+ // the regular text table, use the reference.
+ // Specify null here so the so the text is
+ // dereferenced for page length info if needed.
+ $revText = null;
+ } else {
+ // Revision was deleted in 1.4 or earlier.
+ // Text is squashed into the archive row, and
+ // a new text table entry will be created for it.
+ $revText = Revision::getRevisionText( $row, 'ar_' );
+ }
+ $revision = new Revision( array(
+ 'page' => $pageId,
+ 'id' => $row->ar_rev_id,
+ 'text' => $revText,
+ 'comment' => $row->ar_comment,
+ 'user' => $row->ar_user,
+ 'user_text' => $row->ar_user_text,
+ 'timestamp' => $row->ar_timestamp,
+ 'minor_edit' => $row->ar_minor_edit,
+ 'text_id' => $row->ar_text_id,
+ 'deleted' => $unsuppress ? 0 : $row->ar_deleted,
+ 'len' => $row->ar_len
+ ) );
+ $revision->insertOn( $dbw );
+ $restored++;
+
+ wfRunHooks( 'ArticleRevisionUndeleted', array( &$this->title, $revision, $row->ar_page_id ) );
+ }
+ // Was anything restored at all?
+ 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 );
+ }
+
+ if( $newid ) {
+ wfRunHooks( 'ArticleUndelete', array( &$this->title, true ) );
+ Article::onArticleCreate( $this->title );
+ } else {
+ wfRunHooks( 'ArticleUndelete', array( &$this->title, false ) );
+ Article::onArticleEdit( $this->title );
+ }
+
+ if( $this->title->getNamespace() == NS_IMAGE ) {
+ $update = new HTMLCacheUpdate( $this->title, 'imagelinks' );
+ $update->doUpdate();
+ }
+ } else {
+ // Revision couldn't be created. This is very weird
+ 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;
+ }
+
+ function getFileStatus() { return $this->fileStatus; }
+}
+
+/**
+ * The HTML form for Special:Undelete, which allows users with the appropriate
+ * permissions to view and restore deleted content.
+ * @ingroup SpecialPage
+ */
+class UndeleteForm {
+ var $mAction, $mTarget, $mTimestamp, $mRestore, $mTargetObj;
+ var $mTargetTimestamp, $mAllowed, $mComment;
+
+ function UndeleteForm( $request, $par = "" ) {
+ global $wgUser;
+ $this->mAction = $request->getVal( 'action' );
+ $this->mTarget = $request->getVal( 'target' );
+ $this->mSearchPrefix = $request->getText( 'prefix' );
+ $time = $request->getVal( 'timestamp' );
+ $this->mTimestamp = $time ? wfTimestamp( TS_MW, $time ) : '';
+ $this->mFile = $request->getVal( 'file' );
+
+ $posted = $request->wasPosted() &&
+ $wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) );
+ $this->mRestore = $request->getCheck( 'restore' ) && $posted;
+ $this->mPreview = $request->getCheck( 'preview' ) && $posted;
+ $this->mDiff = $request->getCheck( 'diff' );
+ $this->mComment = $request->getText( 'wpComment' );
+ $this->mUnsuppress = $request->getVal( 'wpUnsuppress' ) && $wgUser->isAllowed( 'suppressrevision' );
+
+ if( $par != "" ) {
+ $this->mTarget = $par;
+ }
+ if ( $wgUser->isAllowed( 'undelete' ) && !$wgUser->isBlocked() ) {
+ $this->mAllowed = true;
+ } else {
+ $this->mAllowed = false;
+ $this->mTimestamp = '';
+ $this->mRestore = false;
+ }
+ if ( $this->mTarget !== "" ) {
+ $this->mTargetObj = Title::newFromURL( $this->mTarget );
+ } else {
+ $this->mTargetObj = NULL;
+ }
+ if( $this->mRestore ) {
+ $timestamps = array();
+ $this->mFileVersions = array();
+ foreach( $_REQUEST as $key => $val ) {
+ $matches = array();
+ if( preg_match( '/^ts(\d{14})$/', $key, $matches ) ) {
+ array_push( $timestamps, $matches[1] );
+ }
+
+ if( preg_match( '/^fileid(\d+)$/', $key, $matches ) ) {
+ $this->mFileVersions[] = intval( $matches[1] );
+ }
+ }
+ rsort( $timestamps );
+ $this->mTargetTimestamp = $timestamps;
+ }
+ }
+
+ function execute() {
+ global $wgOut, $wgUser;
+ if ( $this->mAllowed ) {
+ $wgOut->setPagetitle( wfMsg( "undeletepage" ) );
+ } else {
+ $wgOut->setPagetitle( wfMsg( "viewdeletedpage" ) );
+ }
+
+ if( is_null( $this->mTargetObj ) ) {
+ # Not all users can just browse every deleted page from the list
+ if( $wgUser->isAllowed( 'browsearchive' ) ) {
+ $this->showSearchForm();
+
+ # List undeletable articles
+ if( $this->mSearchPrefix ) {
+ $result = PageArchive::listPagesByPrefix( $this->mSearchPrefix );
+ $this->showList( $result );
+ }
+ } else {
+ $wgOut->addWikiText( wfMsgHtml( 'undelete-header' ) );
+ }
+ return;
+ }
+ if( $this->mTimestamp !== '' ) {
+ return $this->showRevision( $this->mTimestamp );
+ }
+ if( $this->mFile !== null ) {
+ $file = new ArchivedFile( $this->mTargetObj, '', $this->mFile );
+ // Check if user is allowed to see this file
+ if( !$file->userCan( File::DELETED_FILE ) ) {
+ $wgOut->permissionRequired( 'suppressrevision' );
+ return false;
+ } else {
+ return $this->showFile( $this->mFile );
+ }
+ }
+ if( $this->mRestore && $this->mAction == "submit" ) {
+ return $this->undelete();
+ }
+ return $this->showHistory();
+ }
+
+ function showSearchForm() {
+ global $wgOut, $wgScript;
+ $wgOut->addWikiMsg( 'undelete-header' );
+
+ $wgOut->addHtml(
+ Xml::openElement( 'form', array(
+ 'method' => 'get',
+ 'action' => $wgScript ) ) .
+ '<fieldset>' .
+ Xml::element( 'legend', array(),
+ wfMsg( 'undelete-search-box' ) ) .
+ Xml::hidden( 'title',
+ SpecialPage::getTitleFor( 'Undelete' )->getPrefixedDbKey() ) .
+ Xml::inputLabel( wfMsg( 'undelete-search-prefix' ),
+ 'prefix', 'prefix', 20,
+ $this->mSearchPrefix ) .
+ Xml::submitButton( wfMsg( 'undelete-search-submit' ) ) .
+ '</fieldset>' .
+ '</form>' );
+ }
+
+ // Generic list of deleted pages
+ private function showList( $result ) {
+ global $wgLang, $wgContLang, $wgUser, $wgOut;
+
+ if( $result->numRows() == 0 ) {
+ $wgOut->addWikiMsg( 'undelete-no-results' );
+ return;
+ }
+
+ $wgOut->addWikiMsg( "undeletepagetext" );
+
+ $sk = $wgUser->getSkin();
+ $undelete = SpecialPage::getTitleFor( 'Undelete' );
+ $wgOut->addHTML( "<ul>\n" );
+ while( $row = $result->fetchObject() ) {
+ $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" );
+ }
+ $result->free();
+ $wgOut->addHTML( "</ul>\n" );
+
+ return true;
+ }
+
+ private function showRevision( $timestamp ) {
+ global $wgLang, $wgUser, $wgOut;
+ $self = SpecialPage::getTitleFor( 'Undelete' );
+ $skin = $wgUser->getSkin();
+
+ if(!preg_match("/[0-9]{14}/",$timestamp)) return 0;
+
+ $archive = new PageArchive( $this->mTargetObj );
+ $rev = $archive->getRevision( $timestamp );
+
+ if( !$rev ) {
+ $wgOut->addWikiMsg( 'undeleterevision-missing' );
+ return;
+ }
+
+ if( $rev->isDeleted(Revision::DELETED_TEXT) ) {
+ if( !$rev->userCan(Revision::DELETED_TEXT) ) {
+ $wgOut->addWikiText( wfMsg( 'rev-deleted-text-permission' ) );
+ return;
+ } else {
+ $wgOut->addWikiText( wfMsg( 'rev-deleted-text-view' ) );
+ $wgOut->addHTML( '<br/>' );
+ // and we are allowed to see...
+ }
+ }
+
+ $wgOut->setPageTitle( wfMsg( 'undeletepage' ) );
+
+ $link = $skin->makeKnownLinkObj(
+ 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 );
+ if( $previousRev ) {
+ $this->showDiff( $previousRev, $rev );
+ if( $wgUser->getOption( 'diffonly' ) ) {
+ return;
+ } else {
+ $wgOut->addHtml( '<hr />' );
+ }
+ } else {
+ $wgOut->addHtml( wfMsgHtml( 'undelete-nodiff' ) );
+ }
+ }
+
+ $wgOut->addHtml( '<p>' . wfMsgHtml( 'undelete-revision', $link, $time, $user ) . '</p>' );
+
+ wfRunHooks( 'UndeleteShowRevision', array( $this->mTargetObj, $rev ) );
+
+ if( $this->mPreview ) {
+ $wgOut->addHtml( "<hr />\n" );
+
+ //Hide [edit]s
+ $popts = $wgOut->parserOptions();
+ $popts->setEditSection( false );
+ $wgOut->parserOptions( $popts );
+ $wgOut->addWikiTextTitleTidy( $rev->revText(), $this->mTargetObj, true );
+ }
+
+ $wgOut->addHtml(
+ wfElement( 'textarea', array(
+ 'readonly' => 'readonly',
+ 'cols' => intval( $wgUser->getOption( 'cols' ) ),
+ 'rows' => intval( $wgUser->getOption( 'rows' ) ) ),
+ $rev->revText() . "\n" ) .
+ wfOpenElement( 'div' ) .
+ wfOpenElement( 'form', array(
+ 'method' => 'post',
+ 'action' => $self->getLocalURL( "action=submit" ) ) ) .
+ wfElement( 'input', array(
+ 'type' => 'hidden',
+ 'name' => 'target',
+ 'value' => $this->mTargetObj->getPrefixedDbKey() ) ) .
+ wfElement( 'input', array(
+ 'type' => 'hidden',
+ 'name' => 'timestamp',
+ 'value' => $timestamp ) ) .
+ wfElement( 'input', array(
+ 'type' => 'hidden',
+ 'name' => 'wpEditToken',
+ 'value' => $wgUser->editToken() ) ) .
+ wfElement( 'input', array(
+ 'type' => 'submit',
+ 'name' => 'preview',
+ 'value' => wfMsg( 'showpreview' ) ) ) .
+ wfElement( 'input', array(
+ 'name' => 'diff',
+ 'type' => 'submit',
+ 'value' => wfMsg( 'showdiff' ) ) ) .
+ wfCloseElement( 'form' ) .
+ wfCloseElement( 'div' ) );
+ }
+
+ /**
+ * Build a diff display between this and the previous either deleted
+ * or non-deleted edit.
+ * @param Revision $previousRev
+ * @param Revision $currentRev
+ * @return string HTML
+ */
+ function showDiff( $previousRev, $currentRev ) {
+ global $wgOut, $wgUser;
+
+ $diffEngine = new DifferenceEngine();
+ $diffEngine->showDiffStyle();
+ $wgOut->addHtml(
+ "<div>" .
+ "<table border='0' width='98%' cellpadding='0' cellspacing='4' class='diff'>" .
+ "<col class='diff-marker' />" .
+ "<col class='diff-content' />" .
+ "<col class='diff-marker' />" .
+ "<col class='diff-content' />" .
+ "<tr>" .
+ "<td colspan='2' width='50%' align='center' class='diff-otitle'>" .
+ $this->diffHeader( $previousRev ) .
+ "</td>" .
+ "<td colspan='2' width='50%' align='center' class='diff-ntitle'>" .
+ $this->diffHeader( $currentRev ) .
+ "</td>" .
+ "</tr>" .
+ $diffEngine->generateDiffBody(
+ $previousRev->getText(), $currentRev->getText() ) .
+ "</table>" .
+ "</div>\n" );
+
+ }
+
+ private function diffHeader( $rev ) {
+ global $wgUser, $wgLang, $wgLang;
+ $sk = $wgUser->getSkin();
+ $isDeleted = !( $rev->getId() && $rev->getTitle() );
+ if( $isDeleted ) {
+ /// @fixme $rev->getTitle() is null for deleted revs...?
+ $targetPage = SpecialPage::getTitleFor( 'Undelete' );
+ $targetQuery = 'target=' .
+ $this->mTargetObj->getPrefixedUrl() .
+ '&timestamp=' .
+ wfTimestamp( TS_MW, $rev->getTimestamp() );
+ } else {
+ /// @fixme getId() may return non-zero for deleted revs...
+ $targetPage = $rev->getTitle();
+ $targetQuery = 'oldid=' . $rev->getId();
+ }
+ return
+ '<div id="mw-diff-otitle1"><strong>' .
+ $sk->makeLinkObj( $targetPage,
+ wfMsgHtml( 'revisionasof',
+ $wgLang->timeanddate( $rev->getTimestamp(), true ) ),
+ $targetQuery ) .
+ ( $isDeleted ? ' ' . wfMsgHtml( 'deletedrev' ) : '' ) .
+ '</strong></div>' .
+ '<div id="mw-diff-otitle2">' .
+ $sk->revUserTools( $rev ) . '<br/>' .
+ '</div>' .
+ '<div id="mw-diff-otitle3">' .
+ $sk->revComment( $rev ) . '<br/>' .
+ '</div>';
+ }
+
+ /**
+ * Show a deleted file version requested by the visitor.
+ */
+ private function showFile( $key ) {
+ global $wgOut, $wgRequest;
+ $wgOut->disable();
+
+ # We mustn't allow the output to be Squid cached, otherwise
+ # if an admin previews a deleted image, and it's cached, then
+ # a user without appropriate permissions can toddle off and
+ # nab the image, and Squid will serve it
+ $wgRequest->response()->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
+ $wgRequest->response()->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
+ $wgRequest->response()->header( 'Pragma: no-cache' );
+
+ $store = FileStore::get( 'deleted' );
+ $store->stream( $key );
+ }
+
+ private function showHistory() {
+ global $wgLang, $wgUser, $wgOut;
+
+ $sk = $wgUser->getSkin();
+ if( $this->mAllowed ) {
+ $wgOut->setPagetitle( wfMsg( "undeletepage" ) );
+ } else {
+ $wgOut->setPagetitle( wfMsg( 'viewdeletedpage' ) );
+ }
+
+ $wgOut->addWikiText( wfMsgHtml( 'undeletepagetitle', $this->mTargetObj->getPrefixedText()) );
+
+ $archive = new PageArchive( $this->mTargetObj );
+ /*
+ $text = $archive->getLastRevisionText();
+ if( is_null( $text ) ) {
+ $wgOut->addWikiMsg( "nohistory" );
+ return;
+ }
+ */
+ if ( $this->mAllowed ) {
+ $wgOut->addWikiMsg( "undeletehistory" );
+ $wgOut->addWikiMsg( "undeleterevdel" );
+ } else {
+ $wgOut->addWikiMsg( "undeletehistorynoadmin" );
+ }
+
+ # List all stored revisions
+ $revisions = $archive->listRevisions();
+ $files = $archive->listFiles();
+
+ $haveRevisions = $revisions && $revisions->numRows() > 0;
+ $haveFiles = $files && $files->numRows() > 0;
+
+ # Batch existence check on user and talk pages
+ if( $haveRevisions ) {
+ $batch = new LinkBatch();
+ while( $row = $revisions->fetchObject() ) {
+ $batch->addObj( Title::makeTitleSafe( NS_USER, $row->ar_user_text ) );
+ $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->ar_user_text ) );
+ }
+ $batch->execute();
+ $revisions->seek( 0 );
+ }
+ if( $haveFiles ) {
+ $batch = new LinkBatch();
+ while( $row = $files->fetchObject() ) {
+ $batch->addObj( Title::makeTitleSafe( NS_USER, $row->fa_user_text ) );
+ $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->fa_user_text ) );
+ }
+ $batch->execute();
+ $files->seek( 0 );
+ }
+
+ if ( $this->mAllowed ) {
+ $titleObj = SpecialPage::getTitleFor( "Undelete" );
+ $action = $titleObj->getLocalURL( "action=submit" );
+ # Start the form here
+ $top = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'undelete' ) );
+ $wgOut->addHtml( $top );
+ }
+
+ # Show relevant lines from the deletion log:
+ $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) . "\n" );
+ LogEventsList::showLogExtract( $wgOut, 'delete', $this->mTargetObj->getPrefixedText() );
+
+ if( $this->mAllowed && ( $haveRevisions || $haveFiles ) ) {
+ # Format the user-visible controls (comment field, submission button)
+ # in a nice little table
+ if( $wgUser->isAllowed( 'suppressrevision' ) ) {
+ $unsuppressBox =
+ "<tr>
+ <td>&nbsp;</td>
+ <td class='mw-input'>" .
+ Xml::checkLabel( wfMsg('revdelete-unsuppress'), 'wpUnsuppress',
+ 'mw-undelete-unsuppress', $this->mUnsuppress ).
+ "</td>
+ </tr>";
+ } else {
+ $unsuppressBox = "";
+ }
+ $table =
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', null, wfMsg( 'undelete-fieldset-title' ) ).
+ Xml::openElement( 'table', array( 'id' => 'mw-undelete-table' ) ) .
+ "<tr>
+ <td colspan='2'>" .
+ wfMsgWikiHtml( 'undeleteextrahelp' ) .
+ "</td>
+ </tr>
+ <tr>
+ <td class='mw-label'>" .
+ Xml::label( wfMsg( 'undeletecomment' ), 'wpComment' ) .
+ "</td>
+ <td class='mw-input'>" .
+ Xml::input( 'wpComment', 50, $this->mComment, array( 'id' => 'wpComment' ) ) .
+ "</td>
+ </tr>
+ <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' ) ) .
+ "</td>
+ </tr>" .
+ $unsuppressBox .
+ Xml::closeElement( 'table' ) .
+ Xml::closeElement( 'fieldset' );
+
+ $wgOut->addHtml( $table );
+ }
+
+ $wgOut->addHTML( Xml::element( 'h2', null, wfMsg( 'history' ) ) . "\n" );
+
+ if( $haveRevisions ) {
+ # The page's stored (deleted) history:
+ $wgOut->addHTML("<ul>");
+ $target = urlencode( $this->mTarget );
+ $remaining = $revisions->numRows();
+ $earliestLiveTime = $this->getEarliestTime( $this->mTargetObj );
+
+ while( $row = $revisions->fetchObject() ) {
+ $remaining--;
+ $wgOut->addHTML( $this->formatRevisionRow( $row, $earliestLiveTime, $remaining, $sk ) );
+ }
+ $revisions->free();
+ $wgOut->addHTML("</ul>");
+ } else {
+ $wgOut->addWikiMsg( "nohistory" );
+ }
+
+ if( $haveFiles ) {
+ $wgOut->addHtml( Xml::element( 'h2', null, wfMsg( 'filehist' ) ) . "\n" );
+ $wgOut->addHtml( "<ul>" );
+ while( $row = $files->fetchObject() ) {
+ $wgOut->addHTML( $this->formatFileRow( $row, $sk ) );
+ }
+ $files->free();
+ $wgOut->addHTML( "</ul>" );
+ }
+
+ if ( $this->mAllowed ) {
+ # Slip in the hidden controls here
+ $misc = Xml::hidden( 'target', $this->mTarget );
+ $misc .= Xml::hidden( 'wpEditToken', $wgUser->editToken() );
+ $misc .= Xml::closeElement( 'form' );
+ $wgOut->addHtml( $misc );
+ }
+
+ return true;
+ }
+
+ private function formatRevisionRow( $row, $earliestLiveTime, $remaining, $sk ) {
+ global $wgUser, $wgLang;
+
+ $rev = new Revision( array(
+ 'page' => $this->mTargetObj->getArticleId(),
+ 'comment' => $row->ar_comment,
+ 'user' => $row->ar_user,
+ 'user_text' => $row->ar_user_text,
+ 'timestamp' => $row->ar_timestamp,
+ 'minor_edit' => $row->ar_minor_edit,
+ 'deleted' => $row->ar_deleted,
+ 'len' => $row->ar_len ) );
+
+ $stxt = '';
+ $ts = wfTimestamp( TS_MW, $row->ar_timestamp );
+ if( $this->mAllowed ) {
+ $checkBox = Xml::check( "ts$ts" );
+ $titleObj = SpecialPage::getTitleFor( "Undelete" );
+ $pageLink = $this->getPageLink( $rev, $titleObj, $ts, $sk );
+ # Last link
+ if( !$rev->userCan( Revision::DELETED_TEXT ) ) {
+ $last = wfMsgHtml('diff');
+ } else if( $remaining > 0 || ($earliestLiveTime && $ts > $earliestLiveTime) ) {
+ $last = $sk->makeKnownLinkObj( $titleObj, wfMsgHtml('diff'),
+ "target=" . $this->mTargetObj->getPrefixedUrl() . "&timestamp=$ts&diff=prev" );
+ } else {
+ $last = wfMsgHtml('diff');
+ }
+ } else {
+ $checkBox = '';
+ $pageLink = $wgLang->timeanddate( $ts, true );
+ $last = wfMsgHtml('diff');
+ }
+ $userLink = $sk->revUserTools( $rev );
+
+ if(!is_null($size = $row->ar_len)) {
+ $stxt = $sk->formatRevisionSize( $size );
+ }
+ $comment = $sk->revComment( $rev );
+ $revdlink = '';
+ if( $wgUser->isAllowed( 'deleterevision' ) ) {
+ $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
+ if( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) {
+ // 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" );
+ // Bolden oversighted content
+ if( $rev->isDeleted( Revision::DELETED_RESTRICTED ) )
+ $del = "<strong>$del</strong>";
+ }
+ $revdlink = "<tt>(<small>$del</small>)</tt>";
+ }
+
+ return "<li>$checkBox $revdlink ($last) $pageLink . . $userLink $stxt $comment</li>";
+ }
+
+ private function formatFileRow( $row, $sk ) {
+ global $wgUser, $wgLang;
+
+ $file = ArchivedFile::newFromRow( $row );
+
+ $ts = wfTimestamp( TS_MW, $row->fa_timestamp );
+ if( $this->mAllowed && $row->fa_storage_key ) {
+ $checkBox = Xml::check( "fileid" . $row->fa_id );
+ $key = urlencode( $row->fa_storage_key );
+ $target = urlencode( $this->mTarget );
+ $titleObj = SpecialPage::getTitleFor( "Undelete" );
+ $pageLink = $this->getFileLink( $file, $titleObj, $ts, $key, $sk );
+ } else {
+ $checkBox = '';
+ $pageLink = $wgLang->timeanddate( $ts, true );
+ }
+ $userLink = $this->getFileUser( $file, $sk );
+ $data =
+ wfMsg( 'widthheight',
+ $wgLang->formatNum( $row->fa_width ),
+ $wgLang->formatNum( $row->fa_height ) ) .
+ ' (' .
+ wfMsg( 'nbytes', $wgLang->formatNum( $row->fa_size ) ) .
+ ')';
+ $data = htmlspecialchars( $data );
+ $comment = $this->getFileComment( $file, $sk );
+ $revdlink = '';
+ if( $wgUser->isAllowed( 'deleterevision' ) ) {
+ $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
+ if( !$file->userCan(File::DELETED_RESTRICTED ) ) {
+ // If revision was hidden from sysops
+ $del = wfMsgHtml('rev-delundel');
+ } else {
+ $del = $sk->makeKnownLinkObj( $revdel,
+ wfMsgHtml('rev-delundel'),
+ 'target=' . $this->mTargetObj->getPrefixedUrl() .
+ '&fileid=' . $row->fa_id );
+ // Bolden oversighted content
+ if( $file->isDeleted( File::DELETED_RESTRICTED ) )
+ $del = "<strong>$del</strong>";
+ }
+ $revdlink = "<tt>(<small>$del</small>)</tt>";
+ }
+ 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
+ */
+ function getPageLink( $rev, $titleObj, $ts, $sk ) {
+ global $wgLang;
+
+ if( !$rev->userCan(Revision::DELETED_TEXT) ) {
+ return '<span class="history-deleted">' . $wgLang->timeanddate( $ts, true ) . '</span>';
+ } else {
+ $link = $sk->makeKnownLinkObj( $titleObj, $wgLang->timeanddate( $ts, true ),
+ "target=".$this->mTargetObj->getPrefixedUrl()."&timestamp=$ts" );
+ if( $rev->isDeleted(Revision::DELETED_TEXT) )
+ $link = '<span class="history-deleted">' . $link . '</span>';
+ return $link;
+ }
+ }
+
+ /**
+ * Fetch image view link if it's available to all users
+ * @return string
+ */
+ function getFileLink( $file, $titleObj, $ts, $key, $sk ) {
+ global $wgLang;
+
+ if( !$file->userCan(File::DELETED_FILE) ) {
+ return '<span class="history-deleted">' . $wgLang->timeanddate( $ts, true ) . '</span>';
+ } else {
+ $link = $sk->makeKnownLinkObj( $titleObj, $wgLang->timeanddate( $ts, true ),
+ "target=".$this->mTargetObj->getPrefixedUrl()."&file=$key" );
+ if( $file->isDeleted(File::DELETED_FILE) )
+ $link = '<span class="history-deleted">' . $link . '</span>';
+ return $link;
+ }
+ }
+
+ /**
+ * Fetch file's user id if it's available to this user
+ * @return string
+ */
+ function getFileUser( $file, $sk ) {
+ if( !$file->userCan(File::DELETED_USER) ) {
+ return '<span class="history-deleted">' . wfMsgHtml( 'rev-deleted-user' ) . '</span>';
+ } else {
+ $link = $sk->userLink( $file->getRawUser(), $file->getRawUserText() ) .
+ $sk->userToolLinks( $file->getRawUser(), $file->getRawUserText() );
+ if( $file->isDeleted(File::DELETED_USER) )
+ $link = '<span class="history-deleted">' . $link . '</span>';
+ return $link;
+ }
+ }
+
+ /**
+ * Fetch file upload comment if it's available to this user
+ * @return string
+ */
+ function getFileComment( $file, $sk ) {
+ if( !$file->userCan(File::DELETED_COMMENT) ) {
+ return '<span class="history-deleted"><span class="comment">' . wfMsgHtml( 'rev-deleted-comment' ) . '</span></span>';
+ } else {
+ $link = $sk->commentBlock( $file->getRawDescription() );
+ if( $file->isDeleted(File::DELETED_COMMENT) )
+ $link = '<span class="history-deleted">' . $link . '</span>';
+ return $link;
+ }
+ }
+
+ function undelete() {
+ global $wgOut, $wgUser;
+ if ( wfReadOnly() ) {
+ $wgOut->readOnlyPage();
+ return;
+ }
+ if( !is_null( $this->mTargetObj ) ) {
+ $archive = new PageArchive( $this->mTargetObj );
+ $ok = $archive->undelete(
+ $this->mTargetTimestamp,
+ $this->mComment,
+ $this->mFileVersions,
+ $this->mUnsuppress );
+
+ if( is_array($ok) ) {
+ if ( $ok[1] ) // Undeleted file count
+ wfRunHooks( 'FileUndeleteComplete', array(
+ $this->mTargetObj, $this->mFileVersions,
+ $wgUser, $this->mComment) );
+
+ $skin = $wgUser->getSkin();
+ $link = $skin->makeKnownLinkObj( $this->mTargetObj );
+ $wgOut->addHtml( wfMsgWikiHtml( 'undeletedpage', $link ) );
+ } else {
+ $wgOut->showFatalError( wfMsg( "cannotundelete" ) );
+ $wgOut->addHtml( '<p>' . wfMsgHtml( "undeleterevdel" ) . '</p>' );
+ }
+
+ // Show file deletion warnings and errors
+ $status = $archive->getFileStatus();
+ if( $status && !$status->isGood() ) {
+ $wgOut->addWikiText( $status->getWikiText( 'undelete-error-short', 'undelete-error-long' ) );
+ }
+ } else {
+ $wgOut->showFatalError( wfMsg( "cannotundelete" ) );
+ }
+ return false;
+ }
+}
diff --git a/includes/specials/SpecialUnlockdb.php b/includes/specials/SpecialUnlockdb.php
new file mode 100644
index 00000000..0bf7e5aa
--- /dev/null
+++ b/includes/specials/SpecialUnlockdb.php
@@ -0,0 +1,107 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ *
+ */
+function wfSpecialUnlockdb() {
+ global $wgUser, $wgOut, $wgRequest;
+
+ if( !$wgUser->isAllowed( 'siteadmin' ) ) {
+ $wgOut->permissionRequired( 'siteadmin' );
+ return;
+ }
+
+ $action = $wgRequest->getVal( 'action' );
+ $f = new DBUnlockForm();
+
+ if ( "success" == $action ) {
+ $f->showSuccess();
+ } else if ( "submit" == $action && $wgRequest->wasPosted() &&
+ $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) {
+ $f->doSubmit();
+ } else {
+ $f->showForm( "" );
+ }
+}
+
+/**
+ * @ingroup SpecialPage
+ */
+class DBUnlockForm {
+ function showForm( $err )
+ {
+ global $wgOut, $wgUser;
+
+ global $wgReadOnlyFile;
+ if( !file_exists( $wgReadOnlyFile ) ) {
+ $wgOut->addWikiMsg( 'databasenotlocked' );
+ return;
+ }
+
+ $wgOut->setPagetitle( wfMsg( "unlockdb" ) );
+ $wgOut->addWikiMsg( "unlockdbtext" );
+
+ if ( "" != $err ) {
+ $wgOut->setSubtitle( wfMsg( "formerror" ) );
+ $wgOut->addHTML( '<p class="error">' . htmlspecialchars( $err ) . "</p>\n" );
+ }
+ $lc = htmlspecialchars( wfMsg( "unlockconfirm" ) );
+ $lb = htmlspecialchars( wfMsg( "unlockbtn" ) );
+ $titleObj = SpecialPage::getTitleFor( "Unlockdb" );
+ $action = $titleObj->escapeLocalURL( "action=submit" );
+ $token = htmlspecialchars( $wgUser->editToken() );
+
+ $wgOut->addHTML( <<<END
+
+<form id="unlockdb" method="post" action="{$action}">
+<table border="0">
+ <tr>
+ <td align="right">
+ <input type="checkbox" name="wpLockConfirm" />
+ </td>
+ <td align="left">{$lc}</td>
+ </tr>
+ <tr>
+ <td>&nbsp;</td>
+ <td align="left">
+ <input type="submit" name="wpLock" value="{$lb}" />
+ </td>
+ </tr>
+</table>
+<input type="hidden" name="wpEditToken" value="{$token}" />
+</form>
+END
+);
+
+ }
+
+ function doSubmit() {
+ global $wgOut, $wgRequest, $wgReadOnlyFile;
+
+ $wpLockConfirm = $wgRequest->getCheck( 'wpLockConfirm' );
+ if ( ! $wpLockConfirm ) {
+ $this->showForm( wfMsg( "locknoconfirm" ) );
+ return;
+ }
+ if ( @! unlink( $wgReadOnlyFile ) ) {
+ $wgOut->showFileDeleteError( $wgReadOnlyFile );
+ return;
+ }
+ $titleObj = SpecialPage::getTitleFor( "Unlockdb" );
+ $success = $titleObj->getFullURL( "action=success" );
+ $wgOut->redirect( $success );
+ }
+
+ function showSuccess() {
+ global $wgOut;
+ global $ip;
+
+ $wgOut->setPagetitle( wfMsg( "unlockdb" ) );
+ $wgOut->setSubtitle( wfMsg( "unlockdbsuccesssub" ) );
+ $wgOut->addWikiMsg( "unlockdbsuccesstext", $ip );
+ }
+}
diff --git a/includes/specials/SpecialUnusedcategories.php b/includes/specials/SpecialUnusedcategories.php
new file mode 100644
index 00000000..406f7944
--- /dev/null
+++ b/includes/specials/SpecialUnusedcategories.php
@@ -0,0 +1,46 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * @ingroup SpecialPage
+ */
+class UnusedCategoriesPage extends QueryPage {
+
+ function isExpensive() { return true; }
+
+ function getName() {
+ return 'Unusedcategories';
+ }
+
+ function getPageHeader() {
+ return wfMsgExt( 'unusedcategoriestext', array( 'parse' ) );
+ }
+
+ function getSQL() {
+ $NScat = NS_CATEGORY;
+ $dbr = wfGetDB( DB_SLAVE );
+ list( $categorylinks, $page ) = $dbr->tableNamesN( 'categorylinks', 'page' );
+ return "SELECT 'Unusedcategories' as type,
+ {$NScat} as namespace, page_title as title, page_title as value
+ FROM $page
+ LEFT JOIN $categorylinks ON page_title=cl_to
+ WHERE cl_from IS NULL
+ AND page_namespace = {$NScat}
+ AND page_is_redirect = 0";
+ }
+
+ function formatResult( $skin, $result ) {
+ $title = Title::makeTitle( NS_CATEGORY, $result->title );
+ return $skin->makeLinkObj( $title, $title->getText() );
+ }
+}
+
+/** constructor */
+function wfSpecialUnusedCategories() {
+ list( $limit, $offset ) = wfCheckLimits();
+ $uc = new UnusedCategoriesPage();
+ return $uc->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialUnusedimages.php b/includes/specials/SpecialUnusedimages.php
new file mode 100644
index 00000000..d71b638f
--- /dev/null
+++ b/includes/specials/SpecialUnusedimages.php
@@ -0,0 +1,60 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * implements Special:Unusedimages
+ * @ingroup SpecialPage
+ */
+class UnusedimagesPage extends ImageQueryPage {
+
+ function isExpensive() { return true; }
+
+ function getName() {
+ return 'Unusedimages';
+ }
+
+ function sortDescending() {
+ return false;
+ }
+ function isSyndicated() { return false; }
+
+ function getSQL() {
+ global $wgCountCategorizedImagesAsUsed;
+ $dbr = wfGetDB( DB_SLAVE );
+
+ if ( $wgCountCategorizedImagesAsUsed ) {
+ list( $page, $image, $imagelinks, $categorylinks ) = $dbr->tableNamesN( 'page', 'image', 'imagelinks', 'categorylinks' );
+
+ return "SELECT 'Unusedimages' as type, 6 as namespace, img_name as title, img_timestamp as value,
+ img_user, img_user_text, img_description
+ 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";
+ } else {
+ list( $image, $imagelinks ) = $dbr->tableNamesN( 'image','imagelinks' );
+
+ return "SELECT 'Unusedimages' as type, 6 as namespace, img_name as title, img_timestamp as value,
+ img_user, img_user_text, img_description
+ FROM $image LEFT JOIN $imagelinks ON img_name=il_to WHERE il_to IS NULL ";
+ }
+ }
+
+ function getPageHeader() {
+ return wfMsgExt( 'unusedimagestext', array( 'parse') );
+ }
+
+}
+
+/**
+ * Entry point
+ */
+function wfSpecialUnusedimages() {
+ list( $limit, $offset ) = wfCheckLimits();
+ $uip = new UnusedimagesPage();
+
+ return $uip->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialUnusedtemplates.php b/includes/specials/SpecialUnusedtemplates.php
new file mode 100644
index 00000000..89acd09c
--- /dev/null
+++ b/includes/specials/SpecialUnusedtemplates.php
@@ -0,0 +1,54 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * implements Special:Unusedtemplates
+ * @author Rob Church <robchur@gmail.com>
+ * @copyright © 2006 Rob Church
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ * @ingroup SpecialPage
+ */
+class UnusedtemplatesPage extends QueryPage {
+
+ function getName() { return( 'Unusedtemplates' ); }
+ function isExpensive() { return true; }
+ function isSyndicated() { return false; }
+ function sortDescending() { return false; }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ list( $page, $templatelinks) = $dbr->tableNamesN( 'page', 'templatelinks' );
+ $sql = "SELECT 'Unusedtemplates' AS type, page_title AS title,
+ page_namespace AS namespace, 0 AS value
+ FROM $page
+ LEFT JOIN $templatelinks
+ ON page_namespace = tl_namespace AND page_title = tl_title
+ WHERE page_namespace = 10 AND tl_from IS NULL
+ AND page_is_redirect = 0";
+ return $sql;
+ }
+
+ function formatResult( $skin, $result ) {
+ $title = Title::makeTitle( NS_TEMPLATE, $result->title );
+ $pageLink = $skin->makeKnownLinkObj( $title, '', 'redirect=no' );
+ $wlhLink = $skin->makeKnownLinkObj(
+ SpecialPage::getTitleFor( 'Whatlinkshere' ),
+ wfMsgHtml( 'unusedtemplateswlh' ),
+ 'target=' . $title->getPrefixedUrl() );
+ return wfSpecialList( $pageLink, $wlhLink );
+ }
+
+ function getPageHeader() {
+ return wfMsgExt( 'unusedtemplatestext', array( 'parse' ) );
+ }
+
+}
+
+function wfSpecialUnusedtemplates() {
+ list( $limit, $offset ) = wfCheckLimits();
+ $utp = new UnusedtemplatesPage();
+ $utp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialUnwatchedpages.php b/includes/specials/SpecialUnwatchedpages.php
new file mode 100644
index 00000000..64ab3729
--- /dev/null
+++ b/includes/specials/SpecialUnwatchedpages.php
@@ -0,0 +1,68 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * A special page that displays a list of pages that are not on anyones watchlist.
+ * Implements Special:Unwatchedpages
+ *
+ * @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
+ */
+class UnwatchedpagesPage extends QueryPage {
+
+ function getName() { return 'Unwatchedpages'; }
+ function isExpensive() { return true; }
+ function isSyndicated() { return false; }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ list( $page, $watchlist ) = $dbr->tableNamesN( 'page', 'watchlist' );
+ $mwns = NS_MEDIAWIKI;
+ return
+ "
+ SELECT
+ 'Unwatchedpages' as type,
+ page_namespace as namespace,
+ page_title as title,
+ page_namespace as value
+ FROM $page
+ LEFT JOIN $watchlist ON wl_namespace = page_namespace AND page_title = wl_title
+ WHERE wl_title IS NULL AND page_is_redirect = 0 AND page_namespace<>$mwns
+ ";
+ }
+
+ function sortDescending() { return false; }
+
+ function formatResult( $skin, $result ) {
+ global $wgContLang;
+
+ $nt = Title::makeTitle( $result->namespace, $result->title );
+ $text = $wgContLang->convert( $nt->getPrefixedText() );
+
+ $plink = $skin->makeKnownLinkObj( $nt, htmlspecialchars( $text ) );
+ $wlink = $skin->makeKnownLinkObj( $nt, wfMsgHtml( 'watch' ), 'action=watch' );
+
+ return wfSpecialList( $plink, $wlink );
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialUnwatchedpages() {
+ global $wgUser, $wgOut;
+
+ if ( ! $wgUser->isAllowed( 'unwatchedpages' ) )
+ return $wgOut->permissionRequired( 'unwatchedpages' );
+
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $wpp = new UnwatchedpagesPage();
+
+ $wpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialUpload.php b/includes/specials/SpecialUpload.php
new file mode 100644
index 00000000..8fe2f52f
--- /dev/null
+++ b/includes/specials/SpecialUpload.php
@@ -0,0 +1,1755 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+
+/**
+ * Entry point
+ */
+function wfSpecialUpload() {
+ global $wgRequest;
+ $form = new UploadForm( $wgRequest );
+ $form->execute();
+}
+
+/**
+ * implements Special:Upload
+ * @ingroup SpecialPage
+ */
+class UploadForm {
+ const SUCCESS = 0;
+ const BEFORE_PROCESSING = 1;
+ const LARGE_FILE_SERVER = 2;
+ const EMPTY_FILE = 3;
+ const MIN_LENGHT_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;
+
+ /**#@+
+ * @access private
+ */
+ var $mComment, $mLicense, $mIgnoreWarning, $mCurlError;
+ var $mDestName, $mTempPath, $mFileSize, $mFileProps;
+ var $mCopyrightStatus, $mCopyrightSource, $mReUpload, $mAction, $mUploadClicked;
+ var $mSrcName, $mSessionKey, $mStashed, $mDesiredDestName, $mRemoveTempFile, $mSourceType;
+ var $mDestWarningAck, $mCurlDestHandle;
+ var $mLocalFile;
+
+ # Placeholders for text injection by hooks (must be HTML)
+ # extensions should take care to _append_ to the present value
+ var $uploadFormTextTop;
+ var $uploadFormTextAfterSummary;
+
+ const SESSION_VERSION = 1;
+ /**#@-*/
+
+ /**
+ * Constructor : initialise object
+ * Get data POSTed through the form and assign them to the object
+ * @param $request Data posted.
+ */
+ function UploadForm( &$request ) {
+ global $wgAllowCopyUploads;
+ $this->mDesiredDestName = $request->getText( 'wpDestFile' );
+ $this->mIgnoreWarning = $request->getCheck( 'wpIgnoreWarning' );
+ $this->mComment = $request->getText( 'wpUploadDescription' );
+
+ if( !$request->wasPosted() ) {
+ # GET requests just give the main form; no data except destination
+ # filename and description
+ return;
+ }
+
+ # Placeholders for text injection by hooks (empty per default)
+ $this->uploadFormTextTop = "";
+ $this->uploadFormTextAfterSummary = "";
+
+ $this->mReUpload = $request->getCheck( 'wpReUpload' );
+ $this->mUploadClicked = $request->getCheck( 'wpUpload' );
+
+ $this->mLicense = $request->getText( 'wpLicense' );
+ $this->mCopyrightStatus = $request->getText( 'wpUploadCopyStatus' );
+ $this->mCopyrightSource = $request->getText( 'wpUploadSource' );
+ $this->mWatchthis = $request->getBool( 'wpWatchthis' );
+ $this->mSourceType = $request->getText( 'wpSourceType' );
+ $this->mDestWarningAck = $request->getText( 'wpDestFileWarningAck' );
+
+ $this->mAction = $request->getVal( 'action' );
+
+ $this->mSessionKey = $request->getInt( 'wpSessionKey' );
+ if( !empty( $this->mSessionKey ) &&
+ isset( $_SESSION['wsUploadData'][$this->mSessionKey]['version'] ) &&
+ $_SESSION['wsUploadData'][$this->mSessionKey]['version'] == self::SESSION_VERSION ) {
+ /**
+ * Confirming a temporarily stashed upload.
+ * We don't want path names to be forged, so we keep
+ * them in the session on the server and just give
+ * an opaque key to the user agent.
+ */
+ $data = $_SESSION['wsUploadData'][$this->mSessionKey];
+ $this->mTempPath = $data['mTempPath'];
+ $this->mFileSize = $data['mFileSize'];
+ $this->mSrcName = $data['mSrcName'];
+ $this->mFileProps = $data['mFileProps'];
+ $this->mCurlError = 0/*UPLOAD_ERR_OK*/;
+ $this->mStashed = true;
+ $this->mRemoveTempFile = false;
+ } else {
+ /**
+ *Check for a newly uploaded file.
+ */
+ if( $wgAllowCopyUploads && $this->mSourceType == 'web' ) {
+ $this->initializeFromUrl( $request );
+ } else {
+ $this->initializeFromUpload( $request );
+ }
+ }
+ }
+
+ /**
+ * Initialize the uploaded file from PHP data
+ * @access private
+ */
+ function initializeFromUpload( $request ) {
+ $this->mTempPath = $request->getFileTempName( 'wpUploadFile' );
+ $this->mFileSize = $request->getFileSize( 'wpUploadFile' );
+ $this->mSrcName = $request->getFileName( 'wpUploadFile' );
+ $this->mCurlError = $request->getUploadError( 'wpUploadFile' );
+ $this->mSessionKey = false;
+ $this->mStashed = false;
+ $this->mRemoveTempFile = false; // PHP will handle this
+ }
+
+ /**
+ * Copy a web file to a temporary file
+ * @access private
+ */
+ function initializeFromUrl( $request ) {
+ global $wgTmpDirectory;
+ $url = $request->getText( 'wpUploadFileURL' );
+ $local_file = tempnam( $wgTmpDirectory, 'WEBUPLOAD' );
+
+ $this->mTempPath = $local_file;
+ $this->mFileSize = 0; # Will be set by curlCopy
+ $this->mCurlError = $this->curlCopy( $url, $local_file );
+ $pathParts = explode( '/', $url );
+ $this->mSrcName = array_pop( $pathParts );
+ $this->mSessionKey = false;
+ $this->mStashed = false;
+
+ // PHP won't auto-cleanup the file
+ $this->mRemoveTempFile = file_exists( $local_file );
+ }
+
+ /**
+ * Safe copy from URL
+ * Returns true if there was an error, false otherwise
+ */
+ private function curlCopy( $url, $dest ) {
+ global $wgUser, $wgOut;
+
+ if( !$wgUser->isAllowed( 'upload_by_url' ) ) {
+ $wgOut->permissionRequired( 'upload_by_url' );
+ return true;
+ }
+
+ # Maybe remove some pasting blanks :-)
+ $url = trim( $url );
+ if( stripos($url, 'http://') !== 0 && stripos($url, 'ftp://') !== 0 ) {
+ # Only HTTP or FTP URLs
+ $wgOut->showErrorPage( 'upload-proto-error', 'upload-proto-error-text' );
+ return true;
+ }
+
+ # Open temporary file
+ $this->mCurlDestHandle = @fopen( $this->mTempPath, "wb" );
+ if( $this->mCurlDestHandle === false ) {
+ # Could not open temporary file to write in
+ $wgOut->showErrorPage( 'upload-file-error', 'upload-file-error-text');
+ return true;
+ }
+
+ $ch = curl_init();
+ curl_setopt( $ch, CURLOPT_HTTP_VERSION, 1.0); # Probably not needed, but apparently can work around some bug
+ curl_setopt( $ch, CURLOPT_TIMEOUT, 10); # 10 seconds timeout
+ curl_setopt( $ch, CURLOPT_LOW_SPEED_LIMIT, 512); # 0.5KB per second minimum transfer speed
+ curl_setopt( $ch, CURLOPT_URL, $url);
+ curl_setopt( $ch, CURLOPT_WRITEFUNCTION, array( $this, 'uploadCurlCallback' ) );
+ curl_exec( $ch );
+ $error = curl_errno( $ch ) ? true : false;
+ $errornum = curl_errno( $ch );
+ // if ( $error ) print curl_error ( $ch ) ; # Debugging output
+ curl_close( $ch );
+
+ fclose( $this->mCurlDestHandle );
+ unset( $this->mCurlDestHandle );
+ if( $error ) {
+ unlink( $dest );
+ if( wfEmptyMsg( "upload-curl-error$errornum", wfMsg("upload-curl-error$errornum") ) )
+ $wgOut->showErrorPage( 'upload-misc-error', 'upload-misc-error-text' );
+ else
+ $wgOut->showErrorPage( "upload-curl-error$errornum", "upload-curl-error$errornum-text" );
+ }
+
+ return $error;
+ }
+
+ /**
+ * Callback function for CURL-based web transfer
+ * Write data to file unless we've passed the length limit;
+ * if so, abort immediately.
+ * @access private
+ */
+ function uploadCurlCallback( $ch, $data ) {
+ global $wgMaxUploadSize;
+ $length = strlen( $data );
+ $this->mFileSize += $length;
+ if( $this->mFileSize > $wgMaxUploadSize ) {
+ return 0;
+ }
+ fwrite( $this->mCurlDestHandle, $data );
+ return $length;
+ }
+
+ /**
+ * Start doing stuff
+ * @access public
+ */
+ function execute() {
+ global $wgUser, $wgOut;
+ global $wgEnableUploads;
+
+ # Check uploading enabled
+ if( !$wgEnableUploads ) {
+ $wgOut->showErrorPage( 'uploaddisabled', 'uploaddisabledtext', array( $this->mDesiredDestName ) );
+ return;
+ }
+
+ # Check permissions
+ if( !$wgUser->isAllowed( 'upload' ) ) {
+ if( !$wgUser->isLoggedIn() ) {
+ $wgOut->showErrorPage( 'uploadnologin', 'uploadnologintext' );
+ } else {
+ $wgOut->permissionRequired( 'upload' );
+ }
+ return;
+ }
+
+ # Check blocks
+ if( $wgUser->isBlocked() ) {
+ $wgOut->blockedPage();
+ return;
+ }
+
+ if( wfReadOnly() ) {
+ $wgOut->readOnlyPage();
+ return;
+ }
+
+ if( $this->mReUpload ) {
+ if( !$this->unsaveUploadedFile() ) {
+ return;
+ }
+ # Because it is probably checked and shouldn't be
+ $this->mIgnoreWarning = false;
+
+ $this->mainUploadForm();
+ } else if( 'submit' == $this->mAction || $this->mUploadClicked ) {
+ $this->processUpload();
+ } else {
+ $this->mainUploadForm();
+ }
+
+ $this->cleanupTempFile();
+ }
+
+ /**
+ * Do the upload
+ * Checks are made in SpecialUpload::execute()
+ *
+ * @access private
+ */
+ function processUpload(){
+ global $wgUser, $wgOut, $wgFileExtensions, $wgLang;
+ $details = null;
+ $value = null;
+ $value = $this->internalProcessUpload( $details );
+
+ switch($value) {
+ case self::SUCCESS:
+ $wgOut->redirect( $this->mLocalFile->getTitle()->getFullURL() );
+ break;
+
+ case self::BEFORE_PROCESSING:
+ break;
+
+ case self::LARGE_FILE_SERVER:
+ $this->mainUploadForm( wfMsgHtml( 'largefileserver' ) );
+ break;
+
+ case self::EMPTY_FILE:
+ $this->mainUploadForm( wfMsgHtml( 'emptyfile' ) );
+ break;
+
+ case self::MIN_LENGHT_PARTNAME:
+ $this->mainUploadForm( wfMsgHtml( 'minlength1' ) );
+ break;
+
+ case self::ILLEGAL_FILENAME:
+ $filtered = $details['filtered'];
+ $this->uploadError( wfMsgWikiHtml( 'illegalfilename', htmlspecialchars( $filtered ) ) );
+ break;
+
+ case self::PROTECTED_PAGE:
+ $wgOut->showPermissionsErrorPage( $details['permissionserrors'] );
+ break;
+
+ case self::OVERWRITE_EXISTING_FILE:
+ $errorText = $details['overwrite'];
+ $this->uploadError( $wgOut->parse( $errorText ) );
+ break;
+
+ case self::FILETYPE_MISSING:
+ $this->uploadError( wfMsgExt( 'filetype-missing', array ( 'parseinline' ) ) );
+ break;
+
+ case self::FILETYPE_BADTYPE:
+ $finalExt = $details['finalExt'];
+ $this->uploadError(
+ wfMsgExt( 'filetype-banned-type',
+ array( 'parseinline' ),
+ htmlspecialchars( $finalExt ),
+ implode(
+ wfMsgExt( 'comma-separator', array( 'escapenoentities' ) ),
+ $wgFileExtensions
+ ),
+ $wgLang->formatNum( count($wgFileExtensions) )
+ )
+ );
+ break;
+
+ case self::VERIFICATION_ERROR:
+ $veri = $details['veri'];
+ $this->uploadError( $veri->toString() );
+ break;
+
+ case self::UPLOAD_VERIFICATION_ERROR:
+ $error = $details['error'];
+ $this->uploadError( $error );
+ break;
+
+ case self::UPLOAD_WARNING:
+ $warning = $details['warning'];
+ $this->uploadWarning( $warning );
+ break;
+
+ case self::INTERNAL_ERROR:
+ $internal = $details['internal'];
+ $this->showError( $internal );
+ break;
+
+ default:
+ throw new MWException( __METHOD__ . ": Unknown value `{$value}`" );
+ }
+ }
+
+ /**
+ * Really do the upload
+ * Checks are made in SpecialUpload::execute()
+ *
+ * @param array $resultDetails contains result-specific dict of additional values
+ *
+ * @access private
+ */
+ function internalProcessUpload( &$resultDetails ) {
+ global $wgUser;
+
+ if( !wfRunHooks( 'UploadForm:BeforeProcessing', array( &$this ) ) )
+ {
+ wfDebug( "Hook 'UploadForm:BeforeProcessing' broke processing the file." );
+ return self::BEFORE_PROCESSING;
+ }
+
+ /**
+ * If there was no filename or a zero size given, give up quick.
+ */
+ if( trim( $this->mSrcName ) == '' || empty( $this->mFileSize ) ) {
+ return self::EMPTY_FILE;
+ }
+
+ /* Check for curl error */
+ if( $this->mCurlError ) {
+ return self::BEFORE_PROCESSING;
+ }
+
+ /**
+ * 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.
+ */
+ if( $this->mDesiredDestName ) {
+ $basename = $this->mDesiredDestName;
+ } else {
+ $basename = $this->mSrcName;
+ }
+ $filtered = wfStripIllegalFilenameChars( $basename );
+
+ /**
+ * We'll want to blacklist against *any* 'extension', and use
+ * only the final one for the whitelist.
+ */
+ list( $partname, $ext ) = $this->splitExtensions( $filtered );
+
+ if( count( $ext ) ) {
+ $finalExt = $ext[count( $ext ) - 1];
+ } else {
+ $finalExt = '';
+ }
+
+ # 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 ) {
+ return self::MIN_LENGHT_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();
+
+ /**
+ * If the image is protected, non-sysop users won't be able
+ * to modify it by uploading a new revision.
+ */
+ $permErrors = $nt->getUserPermissionsErrors( 'edit', $wgUser );
+ $permErrorsUpload = $nt->getUserPermissionsErrors( 'upload', $wgUser );
+ $permErrorsCreate = ( $nt->exists() ? array() : $nt->getUserPermissionsErrors( 'create', $wgUser ) );
+
+ if( $permErrors || $permErrorsUpload || $permErrorsCreate ) {
+ // merge all the problems into one list, avoiding duplicates
+ $permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsUpload, $permErrors ) );
+ $permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsCreate, $permErrors ) );
+ $resultDetails = array( 'permissionserrors' => $permErrors );
+ return self::PROTECTED_PAGE;
+ }
+
+ /**
+ * In some cases we may forbid overwriting of existing files.
+ */
+ $overwrite = $this->checkOverwrite( $this->mDestName );
+ if( $overwrite !== true ) {
+ $resultDetails = array( 'overwrite' => $overwrite );
+ return self::OVERWRITE_EXISTING_FILE;
+ }
+
+ /* Don't allow users to override the blacklist (check file extension) */
+ global $wgCheckFileExtensions, $wgStrictFileExtensions;
+ global $wgFileExtensions, $wgFileBlacklist;
+ if ($finalExt == '') {
+ return self::FILETYPE_MISSING;
+ } elseif ( $this->checkFileExtensionList( $ext, $wgFileBlacklist ) ||
+ ($wgCheckFileExtensions && $wgStrictFileExtensions &&
+ !$this->checkFileExtension( $finalExt, $wgFileExtensions ) ) ) {
+ $resultDetails = array( 'finalExt' => $finalExt );
+ return self::FILETYPE_BADTYPE;
+ }
+
+ /**
+ * 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.
+ */
+ if( !$this->mStashed ) {
+ $this->mFileProps = File::getPropsFromPath( $this->mTempPath, $finalExt );
+ $this->checkMacBinary();
+ $veri = $this->verify( $this->mTempPath, $finalExt );
+
+ if( $veri !== true ) { //it's a wiki error...
+ $resultDetails = array( 'veri' => $veri );
+ return self::VERIFICATION_ERROR;
+ }
+
+ /**
+ * Provide an opportunity for extensions to add further checks
+ */
+ $error = '';
+ if( !wfRunHooks( 'UploadVerification',
+ array( $this->mDestName, $this->mTempPath, &$error ) ) ) {
+ $resultDetails = array( 'error' => $error );
+ return self::UPLOAD_VERIFICATION_ERROR;
+ }
+ }
+
+
+ /**
+ * Check for non-fatal conditions
+ */
+ if ( ! $this->mIgnoreWarning ) {
+ $warning = '';
+
+ global $wgCapitalLinks;
+ if( $wgCapitalLinks ) {
+ $filtered = ucfirst( $filtered );
+ }
+ if( $basename != $filtered ) {
+ $warning .= '<li>'.wfMsgHtml( 'badfilename', htmlspecialchars( $this->mDestName ) ).'</li>';
+ }
+
+ global $wgCheckFileExtensions;
+ if ( $wgCheckFileExtensions ) {
+ if ( !$this->checkFileExtension( $finalExt, $wgFileExtensions ) ) {
+ global $wgLang;
+ $warning .= '<li>' .
+ wfMsgExt( 'filetype-unwanted-type',
+ array( 'parseinline' ),
+ htmlspecialchars( $finalExt ),
+ implode(
+ wfMsgExt( 'comma-separator', array( 'escapenoentities' ) ),
+ $wgFileExtensions
+ ),
+ $wgLang->formatNum( count($wgFileExtensions) )
+ ) . '</li>';
+ }
+ }
+
+ global $wgUploadSizeWarning;
+ if ( $wgUploadSizeWarning && ( $this->mFileSize > $wgUploadSizeWarning ) ) {
+ $skin = $wgUser->getSkin();
+ $wsize = $skin->formatSize( $wgUploadSizeWarning );
+ $asize = $skin->formatSize( $this->mFileSize );
+ $warning .= '<li>' . wfMsgHtml( 'large-file', $wsize, $asize ) . '</li>';
+ }
+ if ( $this->mFileSize == 0 ) {
+ $warning .= '<li>'.wfMsgHtml( 'emptyfile' ).'</li>';
+ }
+
+ if ( !$this->mDestWarningAck ) {
+ $warning .= self::getExistsWarning( $this->mLocalFile );
+ }
+
+ $warning .= $this->getDupeWarning( $this->mTempPath );
+
+ if( $warning != '' ) {
+ /**
+ * Stash the file in a temporary location; the user can choose
+ * to let it through and we'll complete the upload then.
+ */
+ $resultDetails = array( 'warning' => $warning );
+ return self::UPLOAD_WARNING;
+ }
+ }
+
+ /**
+ * Try actually saving the thing...
+ * It will show an error form on failure.
+ */
+ $pageText = self::getInitialPageText( $this->mComment, $this->mLicense,
+ $this->mCopyrightStatus, $this->mCopyrightSource );
+
+ $status = $this->mLocalFile->upload( $this->mTempPath, $this->mComment, $pageText,
+ File::DELETE_SOURCE, $this->mFileProps );
+ if ( !$status->isGood() ) {
+ $resultDetails = array( 'internal' => $status->getWikiText() );
+ return self::INTERNAL_ERROR;
+ } else {
+ if ( $this->mWatchthis ) {
+ global $wgUser;
+ $wgUser->addWatch( $this->mLocalFile->getTitle() );
+ }
+ // Success, redirect to description page
+ $img = null; // @todo: added to avoid passing a ref to null - should this be defined somewhere?
+ wfRunHooks( 'UploadComplete', array( &$this ) );
+ return self::SUCCESS;
+ }
+ }
+
+ /**
+ * Do existence checks on a file and produce a warning
+ * This check is static and can be done pre-upload via AJAX
+ * Returns an HTML fragment consisting of one or more LI elements if there is a warning
+ * Returns an empty string if there is no warning
+ */
+ static function getExistsWarning( $file ) {
+ global $wgUser, $wgContLang;
+ // Check for uppercase extension. We allow these filenames but check if an image
+ // with lowercase extension exists already
+ $warning = '';
+ $align = $wgContLang->isRtl() ? 'left' : 'right';
+
+ 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 );
+ }
+
+ $sk = $wgUser->getSkin();
+
+ 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_IMAGE, $partname . '.' . $file->getExtension() );
+ $file_lc = wfLocalFile( $nt_lc );
+ } else {
+ $file_lc = false;
+ }
+
+ if( $file->exists() ) {
+ $dlink = $sk->makeKnownLinkObj( $file->getTitle() );
+ if ( $file->allowInlineDisplay() ) {
+ $dlink2 = $sk->makeImageLinkObj( $file->getTitle(), wfMsgExt( 'fileexists-thumb', 'parseinline' ),
+ $file->getName(), $align, array(), false, true );
+ } elseif ( !$file->allowInlineDisplay() && $file->isSafeFile() ) {
+ $icon = $file->iconThumb();
+ $dlink2 = '<div style="float:' . $align . '" id="mw-media-icon">' .
+ $icon->toHtml( array( 'desc-link' => true ) ) . '<br />' . $dlink . '</div>';
+ } else {
+ $dlink2 = '';
+ }
+
+ $warning .= '<li>' . wfMsgExt( 'fileexists', array('parseinline','replaceafter'), $dlink ) . '</li>' . $dlink2;
+
+ } elseif( $file->getTitle()->getArticleID() ) {
+ $lnk = $sk->makeKnownLinkObj( $file->getTitle(), '', 'redirect=no' );
+ $warning .= '<li>' . wfMsgExt( 'filepageexists', array( 'parseinline', 'replaceafter' ), $lnk ) . '</li>';
+ } elseif ( $file_lc && $file_lc->exists() ) {
+ # Check if image with lowercase extension exists.
+ # It's not forbidden but in 99% it makes no sense to upload the same filename with uppercase extension
+ $dlink = $sk->makeKnownLinkObj( $nt_lc );
+ if ( $file_lc->allowInlineDisplay() ) {
+ $dlink2 = $sk->makeImageLinkObj( $nt_lc, wfMsgExt( 'fileexists-thumb', 'parseinline' ),
+ $nt_lc->getText(), $align, array(), false, true );
+ } elseif ( !$file_lc->allowInlineDisplay() && $file_lc->isSafeFile() ) {
+ $icon = $file_lc->iconThumb();
+ $dlink2 = '<div style="float:' . $align . '" id="mw-media-icon">' .
+ $icon->toHtml( array( 'desc-link' => true ) ) . '<br />' . $dlink . '</div>';
+ } else {
+ $dlink2 = '';
+ }
+
+ $warning .= '<li>' .
+ wfMsgExt( 'fileexists-extension', 'parsemag',
+ $file->getTitle()->getPrefixedText(), $dlink ) .
+ '</li>' . $dlink2;
+
+ } elseif ( ( substr( $partname , 3, 3 ) == 'px-' || substr( $partname , 2, 3 ) == 'px-' )
+ && ereg( "[0-9]{2}" , substr( $partname , 0, 2) ) )
+ {
+ # Check for filenames like 50px- or 180px-, these are mostly thumbnails
+ $nt_thb = Title::newFromText( substr( $partname , strpos( $partname , '-' ) +1 ) . '.' . $rawExtension );
+ $file_thb = wfLocalFile( $nt_thb );
+ if ($file_thb->exists() ) {
+ # Check if an image without leading '180px-' (or similiar) exists
+ $dlink = $sk->makeKnownLinkObj( $nt_thb);
+ if ( $file_thb->allowInlineDisplay() ) {
+ $dlink2 = $sk->makeImageLinkObj( $nt_thb,
+ wfMsgExt( 'fileexists-thumb', 'parseinline' ),
+ $nt_thb->getText(), $align, array(), false, true );
+ } elseif ( !$file_thb->allowInlineDisplay() && $file_thb->isSafeFile() ) {
+ $icon = $file_thb->iconThumb();
+ $dlink2 = '<div style="float:' . $align . '" id="mw-media-icon">' .
+ $icon->toHtml( array( 'desc-link' => true ) ) . '<br />' .
+ $dlink . '</div>';
+ } else {
+ $dlink2 = '';
+ }
+
+ $warning .= '<li>' . wfMsgExt( 'fileexists-thumbnail-yes', 'parsemag', $dlink ) .
+ '</li>' . $dlink2;
+ } else {
+ # Image w/o '180px-' does not exists, but we do not like these filenames
+ $warning .= '<li>' . wfMsgExt( 'file-thumbnail-no', 'parseinline' ,
+ substr( $partname , 0, strpos( $partname , '-' ) +1 ) ) . '</li>';
+ }
+ }
+
+ $filenamePrefixBlacklist = self::getFilenamePrefixBlacklist();
+ # Do the match
+ foreach( $filenamePrefixBlacklist as $prefix ) {
+ if ( substr( $partname, 0, strlen( $prefix ) ) == $prefix ) {
+ $warning .= '<li>' . wfMsgExt( 'filename-bad-prefix', 'parseinline', $prefix ) . '</li>';
+ break;
+ }
+ }
+
+ if ( $file->wasDeleted() && !$file->exists() ) {
+ # If the file existed before and was deleted, warn the user of this
+ # Don't bother doing so if the file exists now, however
+ $ltitle = SpecialPage::getTitleFor( 'Log' );
+ $llink = $sk->makeKnownLinkObj( $ltitle, wfMsgHtml( 'deletionlog' ),
+ 'type=delete&page=' . $file->getTitle()->getPrefixedUrl() );
+ $warning .= '<li>' . wfMsgWikiHtml( 'filewasdeleted', $llink ) . '</li>';
+ }
+ return $warning;
+ }
+
+ /**
+ * Get a list of warnings
+ *
+ * @param string local filename, e.g. 'file exists', 'non-descriptive filename'
+ * @return array list of warning messages
+ */
+ static function ajaxGetExistsWarning( $filename ) {
+ $file = wfFindFile( $filename );
+ if( !$file ) {
+ // Force local file so we have an object to do further checks against
+ // if there isn't an exact match...
+ $file = wfLocalFile( $filename );
+ }
+ $s = '&nbsp;';
+ if ( $file ) {
+ $warning = self::getExistsWarning( $file );
+ if ( $warning !== '' ) {
+ $s = "<ul>$warning</ul>";
+ }
+ }
+ return $s;
+ }
+
+ /**
+ * Render a preview of a given license for the AJAX preview on upload
+ *
+ * @param string $license
+ * @return string
+ */
+ public static function ajaxGetLicensePreview( $license ) {
+ global $wgParser, $wgUser;
+ $text = '{{' . $license . '}}';
+ $title = Title::makeTitle( NS_IMAGE, 'Sample.jpg' );
+ $options = ParserOptions::newFromUser( $wgUser );
+
+ // Expand subst: first, then live templates...
+ $text = $wgParser->preSaveTransform( $text, $title, $wgUser, $options );
+ $output = $wgParser->parse( $text, $title, $options );
+
+ return $output->getText();
+ }
+
+ /**
+ * Check for duplicate files and throw up a warning before the upload
+ * completes.
+ */
+ function getDupeWarning( $tempfile ) {
+ $hash = File::sha1Base36( $tempfile );
+ $dupes = RepoGroup::singleton()->findBySha1( $hash );
+ if( $dupes ) {
+ global $wgOut;
+ $msg = "<gallery>";
+ foreach( $dupes as $file ) {
+ $title = $file->getTitle();
+ $msg .= $title->getPrefixedText() .
+ "|" . $title->getText() . "\n";
+ }
+ $msg .= "</gallery>";
+ return "<li>" .
+ wfMsgExt( "file-exists-duplicate", array( "parse" ), count( $dupes ) ) .
+ $wgOut->parse( $msg ) .
+ "</li>\n";
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * 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 );
+ if ( !$status->isGood() ) {
+ $this->showError( $status->getWikiText() );
+ return false;
+ } else {
+ return $status->value;
+ }
+ }
+
+ /**
+ * 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() {
+ $stash = $this->saveTempUploadedFile( $this->mDestName, $this->mTempPath );
+
+ if( !$stash ) {
+ # Couldn't save the file.
+ return false;
+ }
+
+ $key = mt_rand( 0, 0x7fffffff );
+ $_SESSION['wsUploadData'][$key] = array(
+ 'mTempPath' => $stash,
+ 'mFileSize' => $this->mFileSize,
+ 'mSrcName' => $this->mSrcName,
+ 'mFileProps' => $this->mFileProps,
+ 'version' => self::SESSION_VERSION,
+ );
+ return $key;
+ }
+
+ /**
+ * Remove a temporarily kept file stashed by saveTempUploadedFile().
+ * @access private
+ * @return success
+ */
+ function unsaveUploadedFile() {
+ global $wgOut;
+ $repo = RepoGroup::singleton()->getLocalRepo();
+ $success = $repo->freeTemp( $this->mTempPath );
+ if ( ! $success ) {
+ $wgOut->showFileDeleteError( $this->mTempPath );
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ /* -------------------------------------------------------------- */
+
+ /**
+ * @param string $error as HTML
+ * @access private
+ */
+ function uploadError( $error ) {
+ global $wgOut;
+ $wgOut->addHTML( '<h2>' . wfMsgHtml( 'uploadwarning' ) . "</h2>\n" );
+ $wgOut->addHTML( '<span class="error">' . $error . '</span>' );
+ }
+
+ /**
+ * There's something wrong with this file, not enough to reject it
+ * totally but we require manual intervention to save it for real.
+ * Stash it away, then present a form asking to confirm or cancel.
+ *
+ * @param string $warning as HTML
+ * @access private
+ */
+ function uploadWarning( $warning ) {
+ global $wgOut;
+ global $wgUseCopyrightUpload;
+
+ $this->mSessionKey = $this->stashSession();
+ if( !$this->mSessionKey ) {
+ # Couldn't save file; an error has been displayed so let's go.
+ return;
+ }
+
+ $wgOut->addHTML( '<h2>' . wfMsgHtml( 'uploadwarning' ) . "</h2>\n" );
+ $wgOut->addHTML( '<ul class="warning">' . $warning . "</ul>\n" );
+
+ $titleObj = SpecialPage::getTitleFor( 'Upload' );
+
+ if ( $wgUseCopyrightUpload ) {
+ $copyright = Xml::hidden( 'wpUploadCopyStatus', $this->mCopyrightStatus ) . "\n" .
+ Xml::hidden( 'wpUploadSource', $this->mCopyrightSource ) . "\n";
+ } else {
+ $copyright = '';
+ }
+
+ $wgOut->addHTML(
+ Xml::openElement( 'form', array( 'method' => 'post', 'action' => $titleObj->getLocalURL( 'action=submit' ),
+ 'enctype' => 'multipart/form-data', 'id' => 'uploadwarning' ) ) . "\n" .
+ Xml::hidden( 'wpIgnoreWarning', '1' ) . "\n" .
+ Xml::hidden( 'wpSessionKey', $this->mSessionKey ) . "\n" .
+ Xml::hidden( 'wpUploadDescription', $this->mComment ) . "\n" .
+ Xml::hidden( 'wpLicense', $this->mLicense ) . "\n" .
+ Xml::hidden( 'wpDestFile', $this->mDesiredDestName ) . "\n" .
+ Xml::hidden( 'wpWatchthis', $this->mWatchthis ) . "\n" .
+ "{$copyright}<br />" .
+ Xml::submitButton( wfMsg( 'ignorewarning' ), array ( 'name' => 'wpUpload', 'id' => 'wpUpload', 'checked' => 'checked' ) ) . ' ' .
+ Xml::submitButton( wfMsg( 'reuploaddesc' ), array ( 'name' => 'wpReUpload', 'id' => 'wpReUpload' ) ) .
+ Xml::closeElement( 'form' ) . "\n"
+ );
+ }
+
+ /**
+ * Displays the main upload form, optionally with a highlighted
+ * error message up at the top.
+ *
+ * @param string $msg as HTML
+ * @access private
+ */
+ function mainUploadForm( $msg='' ) {
+ global $wgOut, $wgUser, $wgLang, $wgMaxUploadSize;
+ global $wgUseCopyrightUpload, $wgUseAjax, $wgAjaxUploadDestCheck, $wgAjaxLicensePreview;
+ global $wgRequest, $wgAllowCopyUploads;
+ global $wgStylePath, $wgStyleVersion;
+
+ $useAjaxDestCheck = $wgUseAjax && $wgAjaxUploadDestCheck;
+ $useAjaxLicensePreview = $wgUseAjax && $wgAjaxLicensePreview;
+
+ $adc = wfBoolToStr( $useAjaxDestCheck );
+ $alp = wfBoolToStr( $useAjaxLicensePreview );
+ $autofill = wfBoolToStr( $this->mDesiredDestName == '' );
+
+ $wgOut->addScript( "<script type=\"text/javascript\">
+wgAjaxUploadDestCheck = {$adc};
+wgAjaxLicensePreview = {$alp};
+wgUploadAutoFill = {$autofill};
+</script>" );
+ $wgOut->addScriptFile( 'upload.js' );
+ $wgOut->addScriptFile( 'edit.js' ); // For <charinsert> support
+
+ if( !wfRunHooks( 'UploadForm:initial', array( &$this ) ) )
+ {
+ wfDebug( "Hook 'UploadForm:initial' broke output of the upload form" );
+ return false;
+ }
+
+ if( $this->mDesiredDestName ) {
+ $title = Title::makeTitleSafe( NS_IMAGE, $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(
+ $wgUser->isAllowed( 'delete' ) ? 'thisisdeleted' : 'viewdeleted',
+ array( 'parse', 'replaceafter' ),
+ $wgUser->getSkin()->makeKnownLinkObj(
+ SpecialPage::getTitleFor( 'Undelete', $title->getPrefixedText() ),
+ wfMsgExt( 'restorelink', array( 'parsemag', 'escape' ), $count )
+ )
+ );
+ $wgOut->addHtml( "<div id=\"contentSub2\">{$link}</div>" );
+ }
+
+ // Show the relevant lines from deletion log (for still deleted files only)
+ if( $title instanceof Title && $title->isDeleted() > 0 && !$title->exists() ) {
+ $this->showDeletionLog( $wgOut, $title->getPrefixedText() );
+ }
+ }
+
+ $cols = intval($wgUser->getOption( 'cols' ));
+
+ if( $wgUser->getOption( 'editwidth' ) ) {
+ $width = " style=\"width:100%\"";
+ } else {
+ $width = '';
+ }
+
+ if ( '' != $msg ) {
+ $sub = wfMsgHtml( 'uploaderror' );
+ $wgOut->addHTML( "<h2>{$sub}</h2>\n" .
+ "<span class='error'>{$msg}</span>\n" );
+ }
+ $wgOut->addHTML( '<div id="uploadtext">' );
+ $wgOut->addWikiMsg( 'uploadtext', $this->mDesiredDestName );
+ $wgOut->addHTML( "</div>\n" );
+
+ # Print a list of allowed file extensions, if so configured. We ignore
+ # MIME type here, it's incomprehensible to most people and too long.
+ global $wgCheckFileExtensions, $wgStrictFileExtensions,
+ $wgFileExtensions, $wgFileBlacklist;
+
+ $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 ) ) .
+ "</div>\n";
+ } else {
+ # We have to list both preferred and prohibited
+ $extensionsList =
+ '<div id="mw-upload-preferred">' .
+ wfMsgWikiHtml( 'upload-preferred', implode( $wgFileExtensions, $delim ) ) .
+ "</div>\n" .
+ '<div id="mw-upload-prohibited">' .
+ wfMsgWikiHtml( 'upload-prohibited', implode( $wgFileBlacklist, $delim ) ) .
+ "</div>\n";
+ }
+ } else {
+ # Everything is permitted.
+ $extensionsList = '';
+ }
+
+ # Get the maximum file size from php.ini as $wgMaxUploadSize works for uploads from URL via CURL only
+ # See http://www.php.net/manual/en/ini.core.php#ini.upload-max-filesize for possible values of upload_max_filesize
+ $val = trim( ini_get( 'upload_max_filesize' ) );
+ $last = strtoupper( ( substr( $val, -1 ) ) );
+ switch( $last ) {
+ case 'G':
+ $val2 = substr( $val, 0, -1 ) * 1024 * 1024 * 1024;
+ break;
+ case 'M':
+ $val2 = substr( $val, 0, -1 ) * 1024 * 1024;
+ break;
+ case 'K':
+ $val2 = substr( $val, 0, -1 ) * 1024;
+ break;
+ default:
+ $val2 = $val;
+ }
+ $val2 = $wgAllowCopyUploads ? min( $wgMaxUploadSize, $val2 ) : $val2;
+ $maxUploadSize = '<div id="mw-upload-maxfilesize">' .
+ wfMsgExt( 'upload-maxfilesize', array( 'parseinline', 'escapenoentities' ),
+ $wgLang->formatSize( $val2 ) ) .
+ "</div>\n";
+
+ $sourcefilename = wfMsgExt( 'sourcefilename', array( 'parseinline', 'escapenoentities' ) );
+ $destfilename = wfMsgExt( 'destfilename', array( 'parseinline', 'escapenoentities' ) );
+
+ $summary = wfMsgExt( 'fileuploadsummary', 'parseinline' );
+
+ $licenses = new Licenses();
+ $license = wfMsgExt( 'license', array( 'parseinline' ) );
+ $nolicense = wfMsgHtml( 'nolicense' );
+ $licenseshtml = $licenses->getHtml();
+
+ $ulb = wfMsgHtml( 'uploadbtn' );
+
+
+ $titleObj = SpecialPage::getTitleFor( 'Upload' );
+
+ $encDestName = htmlspecialchars( $this->mDesiredDestName );
+
+ $watchChecked = $this->watchCheck()
+ ? 'checked="checked"'
+ : '';
+ $warningChecked = $this->mIgnoreWarning ? 'checked' : '';
+
+ // Prepare form for upload or upload/copy
+ if( $wgAllowCopyUploads && $wgUser->isAllowed( 'upload_by_url' ) ) {
+ $filename_form =
+ "<input type='radio' id='wpSourceTypeFile' name='wpSourceType' value='file' " .
+ "onchange='toggle_element_activation(\"wpUploadFileURL\",\"wpUploadFile\")' checked='checked' />" .
+ "<input tabindex='1' type='file' name='wpUploadFile' id='wpUploadFile' " .
+ "onfocus='" .
+ "toggle_element_activation(\"wpUploadFileURL\",\"wpUploadFile\");" .
+ "toggle_element_check(\"wpSourceTypeFile\",\"wpSourceTypeURL\")' " .
+ "onchange='fillDestFilename(\"wpUploadFile\")' size='60' />" .
+ wfMsgHTML( 'upload_source_file' ) . "<br/>" .
+ "<input type='radio' id='wpSourceTypeURL' name='wpSourceType' value='web' " .
+ "onchange='toggle_element_activation(\"wpUploadFile\",\"wpUploadFileURL\")' />" .
+ "<input tabindex='1' type='text' name='wpUploadFileURL' id='wpUploadFileURL' " .
+ "onfocus='" .
+ "toggle_element_activation(\"wpUploadFile\",\"wpUploadFileURL\");" .
+ "toggle_element_check(\"wpSourceTypeURL\",\"wpSourceTypeFile\")' " .
+ "onchange='fillDestFilename(\"wpUploadFileURL\")' size='60' disabled='disabled' />" .
+ wfMsgHtml( 'upload_source_url' ) ;
+ } else {
+ $filename_form =
+ "<input tabindex='1' type='file' name='wpUploadFile' id='wpUploadFile' " .
+ ($this->mDesiredDestName?"":"onchange='fillDestFilename(\"wpUploadFile\")' ") .
+ "size='60' />" .
+ "<input type='hidden' name='wpSourceType' value='file' />" ;
+ }
+ if ( $useAjaxDestCheck ) {
+ $warningRow = "<tr><td colspan='2' id='wpDestFile-warning'>&nbsp;</td></tr>";
+ $destOnkeyup = 'onkeyup="wgUploadWarningObj.keypress();"';
+ } else {
+ $warningRow = '';
+ $destOnkeyup = '';
+ }
+
+ $encComment = htmlspecialchars( $this->mComment );
+
+ $wgOut->addHTML(
+ Xml::openElement( 'form', array( 'method' => 'post', 'action' => $titleObj->getLocalURL(),
+ 'enctype' => 'multipart/form-data', 'id' => 'mw-upload-form' ) ) .
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', null, wfMsg( 'upload' ) ) .
+ Xml::openElement( 'table', array( 'border' => '0', 'id' => 'mw-upload-table' ) ) .
+ "<tr>
+ {$this->uploadFormTextTop}
+ <td class='mw-label'>
+ <label for='wpUploadFile'>{$sourcefilename}</label>
+ </td>
+ <td class='mw-input'>
+ {$filename_form}
+ </td>
+ </tr>
+ <tr>
+ <td></td>
+ <td>
+ {$maxUploadSize}
+ {$extensionsList}
+ </td>
+ </tr>
+ <tr>
+ <td class='mw-label'>
+ <label for='wpDestFile'>{$destfilename}</label>
+ </td>
+ <td class='mw-input'>
+ <input tabindex='2' type='text' name='wpDestFile' id='wpDestFile' size='60'
+ value=\"{$encDestName}\" onchange='toggleFilenameFiller()' $destOnkeyup />
+ </td>
+ </tr>
+ <tr>
+ <td class='mw-label'>
+ <label for='wpUploadDescription'>{$summary}</label>
+ </td>
+ <td class='mw-input'>
+ <textarea tabindex='3' name='wpUploadDescription' id='wpUploadDescription' rows='6'
+ cols='{$cols}'{$width}>$encComment</textarea>
+ {$this->uploadFormTextAfterSummary}
+ </td>
+ </tr>
+ <tr>"
+ );
+
+ if ( $licenseshtml != '' ) {
+ global $wgStylePath;
+ $wgOut->addHTML( "
+ <td class='mw-label'>
+ <label for='wpLicense'>$license</label>
+ </td>
+ <td class='mw-input'>
+ <select name='wpLicense' id='wpLicense' tabindex='4'
+ onchange='licenseSelectorCheck()'>
+ <option value=''>$nolicense</option>
+ $licenseshtml
+ </select>
+ </td>
+ </tr>
+ <tr>"
+ );
+ if( $useAjaxLicensePreview ) {
+ $wgOut->addHtml( "
+ <td></td>
+ <td id=\"mw-license-preview\"></td>
+ </tr>
+ <tr>"
+ );
+ }
+ }
+
+ if ( $wgUseCopyrightUpload ) {
+ $filestatus = wfMsgExt( 'filestatus', 'escapenoentities' );
+ $copystatus = htmlspecialchars( $this->mCopyrightStatus );
+ $filesource = wfMsgExt( 'filesource', 'escapenoentities' );
+ $uploadsource = htmlspecialchars( $this->mCopyrightSource );
+
+ $wgOut->addHTML( "
+ <td class='mw-label' style='white-space: nowrap;'>
+ <label for='wpUploadCopyStatus'>$filestatus</label></td>
+ <td class='mw-input'>
+ <input tabindex='5' type='text' name='wpUploadCopyStatus' id='wpUploadCopyStatus'
+ value=\"$copystatus\" size='60' />
+ </td>
+ </tr>
+ <tr>
+ <td class='mw-label'>
+ <label for='wpUploadCopyStatus'>$filesource</label>
+ </td>
+ <td class='mw-input'>
+ <input tabindex='6' type='text' name='wpUploadSource' id='wpUploadCopyStatus'
+ value=\"$uploadsource\" size='60' />
+ </td>
+ </tr>
+ <tr>"
+ );
+ }
+
+ $wgOut->addHtml( "
+ <td></td>
+ <td>
+ <input tabindex='7' type='checkbox' name='wpWatchthis' id='wpWatchthis' $watchChecked value='true' />
+ <label for='wpWatchthis'>" . wfMsgHtml( 'watchthisupload' ) . "</label>
+ <input tabindex='8' type='checkbox' name='wpIgnoreWarning' id='wpIgnoreWarning' value='true' $warningChecked/>
+ <label for='wpIgnoreWarning'>" . wfMsgHtml( 'ignorewarnings' ) . "</label>
+ </td>
+ </tr>
+ $warningRow
+ <tr>
+ <td></td>
+ <td class='mw-input'>
+ <input tabindex='9' type='submit' name='wpUpload' value=\"{$ulb}\"" . $wgUser->getSkin()->tooltipAndAccesskey( 'upload' ) . " />
+ </td>
+ </tr>
+ <tr>
+ <td></td>
+ <td class='mw-input'>"
+ );
+ $wgOut->addWikiText( wfMsgForContent( 'edittools' ) );
+ $wgOut->addHTML( "
+ </td>
+ </tr>" .
+ Xml::closeElement( 'table' ) .
+ Xml::hidden( 'wpDestFileWarningAck', '', array( 'id' => 'wpDestFileWarningAck' ) ) .
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' )
+ );
+ $uploadfooter = wfMsgNoTrans( 'uploadfooter' );
+ if( $uploadfooter != '-' && !wfEmptyMsg( 'uploadfooter', $uploadfooter ) ){
+ $wgOut->addWikiText( '<div id="mw-upload-footer-message">' . $uploadfooter . '</div>' );
+ }
+ }
+
+ /* -------------------------------------------------------------- */
+
+ /**
+ * See if we should check the 'watch this page' checkbox on the form
+ * based on the user's preferences and whether we're being asked
+ * to create a new file or update an existing one.
+ *
+ * In the case where 'watch edits' is off but 'watch creations' is on,
+ * we'll leave the box unchecked.
+ *
+ * Note that the page target can be changed *on the form*, so our check
+ * state can get out of sync.
+ */
+ function watchCheck() {
+ global $wgUser;
+ if( $wgUser->getOption( 'watchdefault' ) ) {
+ // Watch all edits!
+ return true;
+ }
+
+ $local = wfLocalFile( $this->mDesiredDestName );
+ if( $local && $local->exists() ) {
+ // We're uploading a new version of an existing file.
+ // No creation, so don't watch it if we're not already.
+ return $local->getTitle()->userIsWatching();
+ } else {
+ // New page should get watched if that's our option.
+ return $wgUser->getOption( 'watchcreations' );
+ }
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Verifies that it's ok to include the uploaded file
+ *
+ * @param string $tmpfile the full path of the temporary file to verify
+ * @param string $extension The filename extension that the file is to be served with
+ * @return mixed true of the file is verified, a WikiError object otherwise.
+ */
+ function verify( $tmpfile, $extension ) {
+ #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: <$extension>\n\n");
+ #check mime type against file extension
+ if( !self::verifyExtension( $mime, $extension ) ) {
+ return new WikiErrorMsg( 'uploadcorrupt' );
+ }
+
+ #check mime type blacklist
+ global $wgMimeTypeBlacklist;
+ if( isset($wgMimeTypeBlacklist) && !is_null($wgMimeTypeBlacklist)
+ && $this->checkFileExtension( $mime, $wgMimeTypeBlacklist ) ) {
+ return new WikiErrorMsg( 'filetype-badmime', htmlspecialchars( $mime ) );
+ }
+ }
+
+ #check for htmlish code and javascript
+ if( $this->detectScript ( $tmpfile, $mime, $extension ) ) {
+ return new WikiErrorMsg( 'uploadscripted' );
+ }
+
+ /**
+ * Scan the uploaded file for viruses
+ */
+ $virus= $this->detectVirus($tmpfile);
+ if ( $virus ) {
+ return new WikiErrorMsg( 'uploadvirus', htmlspecialchars($virus) );
+ }
+
+ wfDebug( __METHOD__.": all clear; passing.\n" );
+ return true;
+ }
+
+ /**
+ * 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
+ */
+ 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 binarie 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();
+ }
+
+ /**
+ * 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( "SpecialUpload::cleanupTempFile: Removing temporary file {$this->mTempPath}\n" );
+ unlink( $this->mTempPath );
+ }
+ }
+
+ /**
+ * 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( $name ) {
+ $img = wfFindFile( $name );
+
+ $error = '';
+ if( $img ) {
+ global $wgUser, $wgOut;
+ if( $img->isLocal() ) {
+ if( !self::userCanReUpload( $wgUser, $img->name ) ) {
+ $error = 'fileexists-forbidden';
+ }
+ } else {
+ if( !$wgUser->isAllowed( 'reupload' ) ||
+ !$wgUser->isAllowed( 'reupload-shared' ) ) {
+ $error = "fileexists-shared-forbidden";
+ }
+ }
+ }
+
+ if( $error ) {
+ $errorText = wfMsg( $error, wfEscapeWikiText( $img->getName() ) );
+ return $errorText;
+ }
+
+ // Rockin', go ahead and upload
+ return true;
+ }
+
+ /**
+ * Check if a user is the last uploader
+ *
+ * @param User $user
+ * @param string $img, image name
+ * @return bool
+ */
+ public static function userCanReUpload( User $user, $img ) {
+ if( $user->isAllowed( 'reupload' ) )
+ return true; // non-conditional
+ if( !$user->isAllowed( 'reupload-own' ) )
+ return false;
+
+ $dbr = wfGetDB( DB_SLAVE );
+ $row = $dbr->selectRow('image',
+ /* SELECT */ 'img_user',
+ /* WHERE */ array( 'img_name' => $img )
+ );
+ if ( !$row )
+ return false;
+
+ return $user->getId() == $row->img_user;
+ }
+
+ /**
+ * Display an error with a wikitext description
+ */
+ function showError( $description ) {
+ global $wgOut;
+ $wgOut->setPageTitle( wfMsg( "internalerror" ) );
+ $wgOut->setRobotpolicy( "noindex,nofollow" );
+ $wgOut->setArticleRelated( false );
+ $wgOut->enableClientCache( false );
+ $wgOut->addWikiText( $description );
+ }
+
+ /**
+ * Get the initial image page text based on a comment and optional file status information
+ */
+ static function getInitialPageText( $comment, $license, $copyStatus, $source ) {
+ global $wgUseCopyrightUpload;
+ if ( $wgUseCopyrightUpload ) {
+ if ( $license != '' ) {
+ $licensetxt = '== ' . wfMsgForContent( 'license' ) . " ==\n" . '{{' . $license . '}}' . "\n";
+ }
+ $pageText = '== ' . wfMsg ( 'filedesc' ) . " ==\n" . $comment . "\n" .
+ '== ' . wfMsgForContent ( 'filestatus' ) . " ==\n" . $copyStatus . "\n" .
+ "$licensetxt" .
+ '== ' . wfMsgForContent ( 'filesource' ) . " ==\n" . $source ;
+ } else {
+ if ( $license != '' ) {
+ $filedesc = $comment == '' ? '' : '== ' . wfMsg ( 'filedesc' ) . " ==\n" . $comment . "\n";
+ $pageText = $filedesc .
+ '== ' . wfMsgForContent ( 'license' ) . " ==\n" . '{{' . $license . '}}' . "\n";
+ } else {
+ $pageText = $comment;
+ }
+ }
+ return $pageText;
+ }
+
+ /**
+ * If there are rows in the deletion log for this file, show them,
+ * along with a nice little note for the user
+ *
+ * @param OutputPage $out
+ * @param string filename
+ */
+ private function showDeletionLog( $out, $filename ) {
+ global $wgUser;
+ $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->addWikiMsg( 'upload-wasdeleted' );
+ $out->addHTML(
+ $loglist->beginLogEventsList() .
+ $pager->getBody() .
+ $loglist->endLogEventsList()
+ );
+ $out->addHtml( '</div>' );
+ }
+ }
+}
diff --git a/includes/specials/SpecialUploadMogile.php b/includes/specials/SpecialUploadMogile.php
new file mode 100644
index 00000000..7ff8fda6
--- /dev/null
+++ b/includes/specials/SpecialUploadMogile.php
@@ -0,0 +1,135 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * You will need the extension MogileClient to use this special page.
+ */
+require_once( 'MogileFS.php' );
+
+/**
+ * Entry point
+ */
+function wfSpecialUploadMogile() {
+ global $wgRequest;
+ $form = new UploadFormMogile( $wgRequest );
+ $form->execute();
+}
+
+/**
+ * Extends Special:Upload with MogileFS.
+ * @ingroup SpecialPage
+ */
+class UploadFormMogile extends UploadForm {
+ /**
+ * Move the uploaded file from its temporary location to the final
+ * destination. If a previous version of the file exists, move
+ * it into the archive subdirectory.
+ *
+ * @todo If the later save fails, we may have disappeared the original file.
+ *
+ * @param string $saveName
+ * @param string $tempName full path to the temporary file
+ * @param bool $useRename Not used in this implementation
+ */
+ function saveUploadedFile( $saveName, $tempName, $useRename = false ) {
+ global $wgOut;
+ $mfs = MogileFS::NewMogileFS();
+
+ $this->mSavedFile = "image!{$saveName}";
+
+ if( $mfs->getPaths( $this->mSavedFile )) {
+ $this->mUploadOldVersion = gmdate( 'YmdHis' ) . "!{$saveName}";
+ if( !$mfs->rename( $this->mSavedFile, "archive!{$this->mUploadOldVersion}" ) ) {
+ $wgOut->showFileRenameError( $this->mSavedFile,
+ "archive!{$this->mUploadOldVersion}" );
+ return false;
+ }
+ } else {
+ $this->mUploadOldVersion = '';
+ }
+
+ if ( $this->mStashed ) {
+ if (!$mfs->rename($tempName,$this->mSavedFile)) {
+ $wgOut->showFileRenameError($tempName, $this->mSavedFile );
+ return false;
+ }
+ } else {
+ if ( !$mfs->saveFile($this->mSavedFile,'normal',$tempName )) {
+ $wgOut->showFileCopyError( $tempName, $this->mSavedFile );
+ return false;
+ }
+ unlink($tempName);
+ }
+ return true;
+ }
+
+ /**
+ * 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;
+
+ $stash = 'stash!' . gmdate( "YmdHis" ) . '!' . $saveName;
+ $mfs = MogileFS::NewMogileFS();
+ if ( !$mfs->saveFile( $stash, 'normal', $tempName ) ) {
+ $wgOut->showFileCopyError( $tempName, $stash );
+ return false;
+ }
+ unlink($tempName);
+ return $stash;
+ }
+
+ /**
+ * 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() {
+ $stash = $this->saveTempUploadedFile(
+ $this->mUploadSaveName, $this->mUploadTempName );
+
+ if( !$stash ) {
+ # Couldn't save the file.
+ return false;
+ }
+
+ $key = mt_rand( 0, 0x7fffffff );
+ $_SESSION['wsUploadData'][$key] = array(
+ 'mUploadTempName' => $stash,
+ 'mUploadSize' => $this->mUploadSize,
+ 'mOname' => $this->mOname );
+ return $key;
+ }
+
+ /**
+ * Remove a temporarily kept file stashed by saveTempUploadedFile().
+ * @access private
+ * @return success
+ */
+ function unsaveUploadedFile() {
+ global $wgOut;
+ $mfs = MogileFS::NewMogileFS();
+ if ( ! $mfs->delete( $this->mUploadTempName ) ) {
+ $wgOut->showFileDeleteError( $this->mUploadTempName );
+ return false;
+ } else {
+ return true;
+ }
+ }
+}
diff --git a/includes/specials/SpecialUserlogin.php b/includes/specials/SpecialUserlogin.php
new file mode 100644
index 00000000..27009eed
--- /dev/null
+++ b/includes/specials/SpecialUserlogin.php
@@ -0,0 +1,929 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * constructor
+ */
+function wfSpecialUserlogin( $par = '' ) {
+ global $wgRequest;
+ if( session_id() == '' ) {
+ wfSetupSession();
+ }
+
+ $form = new LoginForm( $wgRequest, $par );
+ $form->execute();
+}
+
+/**
+ * implements Special:Login
+ * @ingroup SpecialPage
+ */
+class LoginForm {
+
+ const SUCCESS = 0;
+ const NO_NAME = 1;
+ const ILLEGAL = 2;
+ const WRONG_PLUGIN_PASS = 3;
+ const NOT_EXISTS = 4;
+ const WRONG_PASS = 5;
+ const EMPTY_PASS = 6;
+ const RESET_PASS = 7;
+ const ABORTED = 8;
+ const CREATE_BLOCKED = 9;
+
+ var $mName, $mPassword, $mRetype, $mReturnTo, $mCookieCheck, $mPosted;
+ var $mAction, $mCreateaccount, $mCreateaccountMail, $mMailmypassword;
+ var $mLoginattempt, $mRemember, $mEmail, $mDomain, $mLanguage, $mSkipCookieCheck;
+
+ /**
+ * Constructor
+ * @param WebRequest $request A WebRequest object passed by reference
+ */
+ function LoginForm( &$request, $par = '' ) {
+ global $wgLang, $wgAllowRealName, $wgEnableEmail;
+ global $wgAuth;
+
+ $this->mType = ( $par == 'signup' ) ? $par : $request->getText( 'type' ); # Check for [[Special:Userlogin/signup]]
+ $this->mName = $request->getText( 'wpName' );
+ $this->mPassword = $request->getText( 'wpPassword' );
+ $this->mRetype = $request->getText( 'wpRetype' );
+ $this->mDomain = $request->getText( 'wpDomain' );
+ $this->mReturnTo = $request->getVal( 'returnto' );
+ $this->mCookieCheck = $request->getVal( 'wpCookieCheck' );
+ $this->mPosted = $request->wasPosted();
+ $this->mCreateaccount = $request->getCheck( 'wpCreateaccount' );
+ $this->mCreateaccountMail = $request->getCheck( 'wpCreateaccountMail' )
+ && $wgEnableEmail;
+ $this->mMailmypassword = $request->getCheck( 'wpMailmypassword' )
+ && $wgEnableEmail;
+ $this->mLoginattempt = $request->getCheck( 'wpLoginattempt' );
+ $this->mAction = $request->getVal( 'action' );
+ $this->mRemember = $request->getCheck( 'wpRemember' );
+ $this->mLanguage = $request->getText( 'uselang' );
+ $this->mSkipCookieCheck = $request->getCheck( 'wpSkipCookieCheck' );
+
+ if( $wgEnableEmail ) {
+ $this->mEmail = $request->getText( 'wpEmail' );
+ } else {
+ $this->mEmail = '';
+ }
+ if( $wgAllowRealName ) {
+ $this->mRealName = $request->getText( 'wpRealName' );
+ } else {
+ $this->mRealName = '';
+ }
+
+ if( !$wgAuth->validDomain( $this->mDomain ) ) {
+ $this->mDomain = 'invaliddomain';
+ }
+ $wgAuth->setDomain( $this->mDomain );
+
+ # When switching accounts, it sucks to get automatically logged out
+ if( $this->mReturnTo == $wgLang->specialPage( 'Userlogout' ) ) {
+ $this->mReturnTo = '';
+ }
+ }
+
+ function execute() {
+ if ( !is_null( $this->mCookieCheck ) ) {
+ $this->onCookieRedirectCheck( $this->mCookieCheck );
+ return;
+ } else if( $this->mPosted ) {
+ if( $this->mCreateaccount ) {
+ return $this->addNewAccount();
+ } else if ( $this->mCreateaccountMail ) {
+ return $this->addNewAccountMailPassword();
+ } else if ( $this->mMailmypassword ) {
+ return $this->mailPassword();
+ } else if ( ( 'submitlogin' == $this->mAction ) || $this->mLoginattempt ) {
+ return $this->processLogin();
+ }
+ }
+ $this->mainLoginForm( '' );
+ }
+
+ /**
+ * @private
+ */
+ function addNewAccountMailPassword() {
+ global $wgOut;
+
+ if ('' == $this->mEmail) {
+ $this->mainLoginForm( wfMsg( 'noemail', htmlspecialchars( $this->mName ) ) );
+ return;
+ }
+
+ $u = $this->addNewaccountInternal();
+
+ if ($u == NULL) {
+ return;
+ }
+
+ // Wipe the initial password and mail a temporary one
+ $u->setPassword( null );
+ $u->saveSettings();
+ $result = $this->mailPasswordInternal( $u, false, 'createaccount-title', 'createaccount-text' );
+
+ wfRunHooks( 'AddNewAccount', array( $u, true ) );
+
+ $wgOut->setPageTitle( wfMsg( 'accmailtitle' ) );
+ $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->setArticleRelated( false );
+
+ if( WikiError::isError( $result ) ) {
+ $this->mainLoginForm( wfMsg( 'mailerror', $result->getMessage() ) );
+ } else {
+ $wgOut->addWikiMsg( 'accmailtext', $u->getName(), $u->getEmail() );
+ $wgOut->returnToMain( false );
+ }
+ $u = 0;
+ }
+
+
+ /**
+ * @private
+ */
+ function addNewAccount() {
+ global $wgUser, $wgEmailAuthentication;
+
+ # Create the account and abort if there's a problem doing so
+ $u = $this->addNewAccountInternal();
+ if( $u == NULL )
+ return;
+
+ # If we showed up language selection links, and one was in use, be
+ # smart (and sensible) and save that language as the user's preference
+ global $wgLoginLanguageSelector;
+ if( $wgLoginLanguageSelector && $this->mLanguage )
+ $u->setOption( 'language', $this->mLanguage );
+
+ # Send out an email authentication message if needed
+ if( $wgEmailAuthentication && User::isValidEmailAddr( $u->getEmail() ) ) {
+ global $wgOut;
+ $error = $u->sendConfirmationMail();
+ if( WikiError::isError( $error ) ) {
+ $wgOut->addWikiMsg( 'confirmemail_sendfailed', $error->getMessage() );
+ } else {
+ $wgOut->addWikiMsg( 'confirmemail_oncreate' );
+ }
+ }
+
+ # 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( $wgUser->isAnon() ) {
+ $wgUser = $u;
+ $wgUser->setCookies();
+ wfRunHooks( 'AddNewAccount', array( $wgUser ) );
+ if( $this->hasSessionCookie() ) {
+ return $this->successfulLogin( 'welcomecreation', $wgUser->getName(), false );
+ } else {
+ return $this->cookieRedirectCheck( 'new' );
+ }
+ } else {
+ # Confirm that the account was created
+ global $wgOut;
+ $self = SpecialPage::getTitleFor( 'Userlogin' );
+ $wgOut->setPageTitle( wfMsgHtml( 'accountcreated' ) );
+ $wgOut->setArticleRelated( false );
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
+ $wgOut->addHtml( wfMsgWikiHtml( 'accountcreatedtext', $u->getName() ) );
+ $wgOut->returnToMain( false, $self );
+ wfRunHooks( 'AddNewAccount', array( $u ) );
+ return true;
+ }
+ }
+
+ /**
+ * @private
+ */
+ function addNewAccountInternal() {
+ global $wgUser, $wgOut;
+ global $wgEnableSorbs, $wgProxyWhitelist;
+ global $wgMemc, $wgAccountCreationThrottle;
+ global $wgAuth, $wgMinimalPasswordLength;
+ global $wgEmailConfirmToEdit;
+
+ // If the user passes an invalid domain, something is fishy
+ if( !$wgAuth->validDomain( $this->mDomain ) ) {
+ $this->mainLoginForm( wfMsg( 'wrongpassword' ) );
+ 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( 'local' != $this->mDomain && '' != $this->mDomain ) {
+ if( !$wgAuth->canCreateAccounts() && ( !$wgAuth->userExists( $this->mName ) || !$wgAuth->authenticate( $this->mName, $this->mPassword ) ) ) {
+ $this->mainLoginForm( wfMsg( 'wrongpassword' ) );
+ return false;
+ }
+ }
+
+ if ( wfReadOnly() ) {
+ $wgOut->readOnlyPage();
+ return false;
+ }
+
+ # Check permissions
+ if ( !$wgUser->isAllowed( 'createaccount' ) ) {
+ $this->userNotPrivilegedMessage();
+ return false;
+ } elseif ( $wgUser->isBlockedFromCreateAccount() ) {
+ $this->userBlockedMessage();
+ return false;
+ }
+
+ $ip = wfGetIP();
+ if ( $wgEnableSorbs && !in_array( $ip, $wgProxyWhitelist ) &&
+ $wgUser->inSorbsBlacklist( $ip ) )
+ {
+ $this->mainLoginForm( wfMsg( 'sorbs_create_account_reason' ) . ' (' . htmlspecialchars( $ip ) . ')' );
+ return;
+ }
+
+ # Now create a dummy user ($u) and check if it is valid
+ $name = trim( $this->mName );
+ $u = User::newFromName( $name, 'creatable' );
+ if ( is_null( $u ) ) {
+ $this->mainLoginForm( wfMsg( 'noname' ) );
+ return false;
+ }
+
+ if ( 0 != $u->idForName() ) {
+ $this->mainLoginForm( wfMsg( 'userexists' ) );
+ return false;
+ }
+
+ if ( 0 != strcmp( $this->mPassword, $this->mRetype ) ) {
+ $this->mainLoginForm( wfMsg( 'badretype' ) );
+ return false;
+ }
+
+ # check for minimal password length
+ if ( !$u->isValidPassword( $this->mPassword ) ) {
+ if ( !$this->mCreateaccountMail ) {
+ $this->mainLoginForm( wfMsgExt( 'passwordtooshort', array( 'parsemag' ), $wgMinimalPasswordLength ) );
+ return false;
+ } else {
+ # do not force a password for account creation by email
+ # set invalid password, it will be replaced later by a random generated password
+ $this->mPassword = null;
+ }
+ }
+
+ # 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;
+ }
+
+ if( !empty( $this->mEmail ) && !User::isValidEmailAddr( $this->mEmail ) ) {
+ $this->mainLoginForm( wfMsg( 'invalidemailaddress' ) );
+ return false;
+ }
+
+ # Set some additional data so the AbortNewAccount hook can be
+ # used for more than just username validation
+ $u->setEmail( $this->mEmail );
+ $u->setRealName( $this->mRealName );
+
+ $abortError = '';
+ if( !wfRunHooks( 'AbortNewAccount', array( $u, &$abortError ) ) ) {
+ // Hook point to add extra creation throttles and blocks
+ wfDebug( "LoginForm::addNewAccountInternal: a hook blocked creation\n" );
+ $this->mainLoginForm( $abortError );
+ return false;
+ }
+
+ if ( $wgAccountCreationThrottle && $wgUser->isPingLimitable() ) {
+ $key = wfMemcKey( 'acctcreate', 'ip', $ip );
+ $value = $wgMemc->incr( $key );
+ if ( !$value ) {
+ $wgMemc->set( $key, 1, 86400 );
+ }
+ if ( $value > $wgAccountCreationThrottle ) {
+ $this->throttleHit( $wgAccountCreationThrottle );
+ return false;
+ }
+ }
+
+ if( !$wgAuth->addUser( $u, $this->mPassword, $this->mEmail, $this->mRealName ) ) {
+ $this->mainLoginForm( wfMsg( 'externaldberror' ) );
+ return false;
+ }
+
+ return $this->initUser( $u, false );
+ }
+
+ /**
+ * Actually add a user to the database.
+ * Give it a User object that has been initialised with a name.
+ *
+ * @param $u User object.
+ * @param $autocreate boolean -- true if this is an autocreation via auth plugin
+ * @return User object.
+ * @private
+ */
+ function initUser( $u, $autocreate ) {
+ global $wgAuth;
+
+ $u->addToDatabase();
+
+ if ( $wgAuth->allowPasswordChange() ) {
+ $u->setPassword( $this->mPassword );
+ }
+
+ $u->setEmail( $this->mEmail );
+ $u->setRealName( $this->mRealName );
+ $u->setToken();
+
+ $wgAuth->initUser( $u, $autocreate );
+
+ $u->setOption( 'rememberpassword', $this->mRemember ? 1 : 0 );
+ $u->saveSettings();
+
+ # Update user count
+ $ssUpdate = new SiteStatsUpdate( 0, 0, 0, 0, 1 );
+ $ssUpdate->doUpdate();
+
+ return $u;
+ }
+
+ /**
+ * Internally authenticate the login request.
+ *
+ * This may create a local account as a side effect if the
+ * authentication plugin allows transparent local account
+ * creation.
+ *
+ * @public
+ */
+ function authenticateUserData() {
+ global $wgUser, $wgAuth;
+ if ( '' == $this->mName ) {
+ return self::NO_NAME;
+ }
+
+ // 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;
+ }
+ $u = User::newFromName( $this->mName );
+ if( is_null( $u ) || !User::isUsableName( $u->getName() ) ) {
+ return self::ILLEGAL;
+ }
+
+ $isAutoCreated = false;
+ if ( 0 == $u->getID() ) {
+ $status = $this->attemptAutoCreate( $u );
+ if ( $status !== self::SUCCESS ) {
+ return $status;
+ } else {
+ $isAutoCreated = true;
+ }
+ } else {
+ $u->load();
+ }
+
+ // Give general extensions, such as a captcha, a chance to abort logins
+ $abort = self::ABORTED;
+ if( !wfRunHooks( 'AbortLogin', array( $u, $this->mPassword, &$abort ) ) ) {
+ return $abort;
+ }
+
+ if (!$u->checkPassword( $this->mPassword )) {
+ if( $u->checkTemporaryPassword( $this->mPassword ) ) {
+ // The e-mailed temporary password should not be used
+ // for actual logins; that's a very sloppy habit,
+ // and insecure if an attacker has a few seconds to
+ // click "search" on someone's open 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.
+ //
+ // 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.
+ //
+ 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.
+ //
+ $retval = self::RESET_PASS;
+ } else {
+ $retval = '' == $this->mPassword ? self::EMPTY_PASS : self::WRONG_PASS;
+ }
+ } else {
+ $wgAuth->updateUser( $u );
+ $wgUser = $u;
+
+ if ( $isAutoCreated ) {
+ // Must be run after $wgUser is set, for correct new user log
+ wfRunHooks( 'AuthPluginAutoCreate', array( $wgUser ) );
+ }
+
+ $retval = self::SUCCESS;
+ }
+ wfRunHooks( 'LoginAuthenticateAudit', array( $u, $this->mPassword, $retval ) );
+ return $retval;
+ }
+
+ /**
+ * 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 ( !$wgAuth->autoCreate() ) {
+ return self::NOT_EXISTS;
+ }
+ if ( !$wgAuth->userExists( $user->getName() ) ) {
+ wfDebug( __METHOD__.": user does not exist\n" );
+ return self::NOT_EXISTS;
+ }
+ if ( !$wgAuth->authenticate( $user->getName(), $this->mPassword ) ) {
+ wfDebug( __METHOD__.": \$wgAuth->authenticate() returned false, aborting\n" );
+ return self::WRONG_PLUGIN_PASS;
+ }
+ if ( $wgUser->isBlockedFromCreateAccount() ) {
+ wfDebug( __METHOD__.": user is blocked from account creation\n" );
+ return self::CREATE_BLOCKED;
+ }
+
+ wfDebug( __METHOD__.": creating account\n" );
+ $user = $this->initUser( $user, true );
+ return self::SUCCESS;
+ }
+
+ function processLogin() {
+ global $wgUser, $wgAuth;
+
+ switch ($this->authenticateUserData())
+ {
+ case self::SUCCESS:
+ # We've verified now, update the real record
+ if( (bool)$this->mRemember != (bool)$wgUser->getOption( 'rememberpassword' ) ) {
+ $wgUser->setOption( 'rememberpassword', $this->mRemember ? 1 : 0 );
+ $wgUser->saveSettings();
+ } else {
+ $wgUser->invalidateCache();
+ }
+ $wgUser->setCookies();
+
+ if( $this->hasSessionCookie() || $this->mSkipCookieCheck ) {
+ /* 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() );
+ } else {
+ return $this->cookieRedirectCheck( 'login' );
+ }
+ break;
+
+ case self::NO_NAME:
+ case self::ILLEGAL:
+ $this->mainLoginForm( wfMsg( 'noname' ) );
+ break;
+ case self::WRONG_PLUGIN_PASS:
+ $this->mainLoginForm( wfMsg( 'wrongpassword' ) );
+ break;
+ case self::NOT_EXISTS:
+ if( $wgUser->isAllowed( 'createaccount' ) ){
+ $this->mainLoginForm( wfMsg( 'nosuchuser', htmlspecialchars( $this->mName ) ) );
+ } else {
+ $this->mainLoginForm( wfMsg( 'nosuchusershort', htmlspecialchars( $this->mName ) ) );
+ }
+ break;
+ case self::WRONG_PASS:
+ $this->mainLoginForm( wfMsg( 'wrongpassword' ) );
+ break;
+ case self::EMPTY_PASS:
+ $this->mainLoginForm( wfMsg( 'wrongpasswordempty' ) );
+ break;
+ case self::RESET_PASS:
+ $this->resetLoginForm( wfMsg( 'resetpass_announce' ) );
+ break;
+ case self::CREATE_BLOCKED:
+ $this->userBlockedMessage();
+ break;
+ default:
+ throw new MWException( "Unhandled case value" );
+ }
+ }
+
+ function resetLoginForm( $error ) {
+ global $wgOut;
+ $wgOut->addWikiText( "<div class=\"errorbox\">$error</div>" );
+ $reset = new PasswordResetForm( $this->mName, $this->mPassword );
+ $reset->execute( null );
+ }
+
+ /**
+ * @private
+ */
+ function mailPassword() {
+ global $wgUser, $wgOut, $wgAuth;
+
+ if( !$wgAuth->allowPasswordChange() ) {
+ $this->mainLoginForm( wfMsg( 'resetpass_forbidden' ) );
+ return;
+ }
+
+ # Check against blocked IPs
+ # fixme -- should we not?
+ if( $wgUser->isBlocked() ) {
+ $this->mainLoginForm( wfMsg( 'blocked-mailpassword' ) );
+ return;
+ }
+
+ # Check against the rate limiter
+ if( $wgUser->pingLimiter( 'mailpassword' ) ) {
+ $wgOut->rateLimited();
+ return;
+ }
+
+ if ( '' == $this->mName ) {
+ $this->mainLoginForm( wfMsg( 'noname' ) );
+ return;
+ }
+ $u = User::newFromName( $this->mName );
+ if( is_null( $u ) ) {
+ $this->mainLoginForm( wfMsg( 'noname' ) );
+ return;
+ }
+ if ( 0 == $u->getID() ) {
+ $this->mainLoginForm( wfMsg( 'nosuchuser', $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.
+ $this->mainLoginForm( wfMsgExt( 'throttled-mailpassword', array( 'parsemag' ),
+ round( $wgPasswordReminderResendTime, 3 ) ) );
+ return;
+ }
+
+ $result = $this->mailPasswordInternal( $u, true, 'passwordremindertitle', 'passwordremindertext' );
+ if( WikiError::isError( $result ) ) {
+ $this->mainLoginForm( wfMsg( 'mailerror', $result->getMessage() ) );
+ } else {
+ $this->mainLoginForm( wfMsg( 'passwordsent', $u->getName() ), 'success' );
+ }
+ }
+
+
+ /**
+ * @param object user
+ * @param bool throttle
+ * @param string message name of email title
+ * @param string message name of email text
+ * @return mixed true on success, WikiError on failure
+ * @private
+ */
+ function mailPasswordInternal( $u, $throttle = true, $emailTitle = 'passwordremindertitle', $emailText = 'passwordremindertext' ) {
+ global $wgCookiePath, $wgCookieDomain, $wgCookiePrefix, $wgCookieSecure;
+ global $wgServer, $wgScript;
+
+ if ( '' == $u->getEmail() ) {
+ return new WikiError( wfMsg( 'noemail', $u->getName() ) );
+ }
+
+ $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 );
+
+ return $result;
+ }
+
+
+ /**
+ * @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
+ * @private
+ */
+ function successfulLogin( $msg, $params, $auto = true ) {
+ global $wgUser;
+ global $wgOut;
+
+ # Run any hooks; ignore results
+
+ $injected_html = '';
+ wfRunHooks('UserLoginComplete', array(&$wgUser, &$injected_html));
+
+ $wgOut->setPageTitle( wfMsg( 'loginsuccesstitle' ) );
+ $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->setArticleRelated( false );
+ $wgOut->addWikiMsgArray( $msg, $params );
+ $wgOut->addHtml( $injected_html );
+ if ( !empty( $this->mReturnTo ) ) {
+ $wgOut->returnToMain( $auto, $this->mReturnTo );
+ } else {
+ $wgOut->returnToMain( $auto );
+ }
+ }
+
+ /** */
+ function userNotPrivilegedMessage($errors) {
+ global $wgOut;
+
+ $wgOut->setPageTitle( wfMsg( 'permissionserrors' ) );
+ $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.
+ $wgOut->addWikiMsg( 'cantcreateaccount-nonblock-text' );
+
+ $wgOut->returnToMain( false );
+ }
+
+ /** */
+ function userBlockedMessage() {
+ global $wgOut, $wgUser;
+
+ # Let's be nice about this, it's likely that this feature will be used
+ # for blocking large numbers of innocent people, e.g. range blocks on
+ # schools. Don't blame it on the user. There's a small chance that it
+ # really is the user's fault, i.e. the username is blocked and they
+ # haven't bothered to log out before trying to create an account to
+ # evade it, but we'll leave that to their guilty conscience to figure
+ # out.
+
+ $wgOut->setPageTitle( wfMsg( 'cantcreateaccounttitle' ) );
+ $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->setArticleRelated( false );
+
+ $ip = wfGetIP();
+ $blocker = User::whoIs( $wgUser->mBlock->mBy );
+ $block_reason = $wgUser->mBlock->mReason;
+
+ if ( strval( $block_reason ) === '' ) {
+ $block_reason = wfMsg( 'blockednoreason' );
+ }
+ $wgOut->addWikiMsg( 'cantcreateaccount-text', $ip, $block_reason, $blocker );
+ $wgOut->returnToMain( false );
+ }
+
+ /**
+ * @private
+ */
+ function mainLoginForm( $msg, $msgtype = 'error' ) {
+ global $wgUser, $wgOut, $wgAllowRealName, $wgEnableEmail;
+ global $wgCookiePrefix, $wgAuth, $wgLoginLanguageSelector;
+ global $wgAuth, $wgEmailConfirmToEdit;
+
+ $titleObj = SpecialPage::getTitleFor( 'Userlogin' );
+
+ if ( $this->mType == 'signup' ) {
+ // Block signup here if in readonly. Keeps user from
+ // going through the process (filling out data, etc)
+ // and being informed later.
+ if ( wfReadOnly() ) {
+ $wgOut->readOnlyPage();
+ return;
+ } elseif ( $wgUser->isBlockedFromCreateAccount() ) {
+ $this->userBlockedMessage();
+ return;
+ } elseif ( count( $permErrors = $titleObj->getUserPermissionsErrors( 'createaccount', $wgUser, true ) )>0 ) {
+ $wgOut->showPermissionsErrorPage( $permErrors, 'createaccount' );
+ return;
+ }
+ }
+
+ if ( '' == $this->mName ) {
+ if ( $wgUser->isLoggedIn() ) {
+ $this->mName = $wgUser->getName();
+ } else {
+ $this->mName = isset( $_COOKIE[$wgCookiePrefix.'UserName'] ) ? $_COOKIE[$wgCookiePrefix.'UserName'] : null;
+ }
+ }
+
+ $titleObj = SpecialPage::getTitleFor( 'Userlogin' );
+
+ if ( $this->mType == 'signup' ) {
+ $template = new UsercreateTemplate();
+ $q = 'action=submitlogin&type=signup';
+ $linkq = 'type=login';
+ $linkmsg = 'gotaccount';
+ } else {
+ $template = new UserloginTemplate();
+ $q = 'action=submitlogin&type=login';
+ $linkq = 'type=signup';
+ $linkmsg = 'nologin';
+ }
+
+ if ( !empty( $this->mReturnTo ) ) {
+ $returnto = '&returnto=' . wfUrlencode( $this->mReturnTo );
+ $q .= $returnto;
+ $linkq .= $returnto;
+ }
+
+ # Pass any language selection on to the mode switch link
+ if( $wgLoginLanguageSelector && $this->mLanguage )
+ $linkq .= '&uselang=' . $this->mLanguage;
+
+ $link = '<a href="' . htmlspecialchars ( $titleObj->getLocalUrl( $linkq ) ) . '">';
+ $link .= wfMsgHtml( $linkmsg . 'link' ); # Calling either 'gotaccountlink' or 'nologinlink'
+ $link .= '</a>';
+
+ # Don't show a "create account" link if the user can't
+ if( $this->showCreateOrLoginLink( $wgUser ) )
+ $template->set( 'link', wfMsgHtml( $linkmsg, $link ) );
+ else
+ $template->set( 'link', '' );
+
+ $template->set( 'header', '' );
+ $template->set( 'name', $this->mName );
+ $template->set( 'password', $this->mPassword );
+ $template->set( 'retype', $this->mRetype );
+ $template->set( 'email', $this->mEmail );
+ $template->set( 'realname', $this->mRealName );
+ $template->set( 'domain', $this->mDomain );
+
+ $template->set( 'action', $titleObj->getLocalUrl( $q ) );
+ $template->set( 'message', $msg );
+ $template->set( 'messagetype', $msgtype );
+ $template->set( 'createemail', $wgEnableEmail && $wgUser->isLoggedIn() );
+ $template->set( 'userealname', $wgAllowRealName );
+ $template->set( 'useemail', $wgEnableEmail );
+ $template->set( 'emailrequired', $wgEmailConfirmToEdit );
+ $template->set( 'canreset', $wgAuth->allowPasswordChange() );
+ $template->set( 'remember', $wgUser->getOption( 'rememberpassword' ) or $this->mRemember );
+
+ # Prepare language selection links as needed
+ if( $wgLoginLanguageSelector ) {
+ $template->set( 'languages', $this->makeLanguageSelector() );
+ if( $this->mLanguage )
+ $template->set( 'uselang', $this->mLanguage );
+ }
+
+ // Give authentication and captcha plugins a chance to modify the form
+ $wgAuth->modifyUITemplate( $template );
+ if ( $this->mType == 'signup' ) {
+ wfRunHooks( 'UserCreateForm', array( &$template ) );
+ } else {
+ wfRunHooks( 'UserLoginForm', array( &$template ) );
+ }
+
+ $wgOut->setPageTitle( wfMsg( 'userlogin' ) );
+ $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->setArticleRelated( false );
+ $wgOut->disallowUserJs(); // just in case...
+ $wgOut->addTemplate( $template );
+ }
+
+ /**
+ * @private
+ */
+ function showCreateOrLoginLink( &$user ) {
+ if( $this->mType == 'signup' ) {
+ return( true );
+ } elseif( $user->isAllowed( 'createaccount' ) ) {
+ return( true );
+ } else {
+ return( false );
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * @private
+ */
+ function hasSessionCookie() {
+ global $wgDisableCookieCheck, $wgRequest;
+ return $wgDisableCookieCheck ? true : $wgRequest->checkSessionCookie();
+ }
+
+ /**
+ * @private
+ */
+ function cookieRedirectCheck( $type ) {
+ global $wgOut;
+
+ $titleObj = SpecialPage::getTitleFor( 'Userlogin' );
+ $check = $titleObj->getFullURL( 'wpCookieCheck='.$type );
+
+ return $wgOut->redirect( $check );
+ }
+
+ /**
+ * @private
+ */
+ function onCookieRedirectCheck( $type ) {
+ global $wgUser;
+
+ if ( !$this->hasSessionCookie() ) {
+ if ( $type == 'new' ) {
+ return $this->mainLoginForm( wfMsgExt( 'nocookiesnew', array( 'parseinline' ) ) );
+ } else if ( $type == 'login' ) {
+ return $this->mainLoginForm( wfMsgExt( 'nocookieslogin', array( 'parseinline' ) ) );
+ } else {
+ # shouldn't happen
+ return $this->mainLoginForm( wfMsg( 'error' ) );
+ }
+ } else {
+ return $this->successfulLogin( 'loginsuccess', $wgUser->getName() );
+ }
+ }
+
+ /**
+ * @private
+ */
+ function throttleHit( $limit ) {
+ global $wgOut;
+
+ $wgOut->addWikiMsg( 'acct_creation_throttle_hit', $limit );
+ }
+
+ /**
+ * Produce a bar of links which allow the user to select another language
+ * during login/registration but retain "returnto"
+ *
+ * @return string
+ */
+ function makeLanguageSelector() {
+ $msg = wfMsgForContent( 'loginlanguagelinks' );
+ if( $msg != '' && !wfEmptyMsg( 'loginlanguagelinks', $msg ) ) {
+ $langs = explode( "\n", $msg );
+ $links = array();
+ foreach( $langs as $lang ) {
+ $lang = trim( $lang, '* ' );
+ $parts = explode( '|', $lang );
+ if (count($parts) >= 2) {
+ $links[] = $this->makeLanguageSelectorLink( $parts[0], $parts[1] );
+ }
+ }
+ return count( $links ) > 0 ? wfMsgHtml( 'loginlanguagelabel', implode( ' | ', $links ) ) : '';
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Create a language selector link for a particular language
+ * Links back to this page preserving type and returnto
+ *
+ * @param $text Link text
+ * @param $lang Language code
+ */
+ function makeLanguageSelectorLink( $text, $lang ) {
+ global $wgUser;
+ $self = SpecialPage::getTitleFor( 'Userlogin' );
+ $attr[] = 'uselang=' . $lang;
+ if( $this->mType == 'signup' )
+ $attr[] = 'type=signup';
+ if( $this->mReturnTo )
+ $attr[] = 'returnto=' . $this->mReturnTo;
+ $skin = $wgUser->getSkin();
+ return $skin->makeKnownLinkObj( $self, htmlspecialchars( $text ), implode( '&', $attr ) );
+ }
+}
diff --git a/includes/specials/SpecialUserlogout.php b/includes/specials/SpecialUserlogout.php
new file mode 100644
index 00000000..137eadb4
--- /dev/null
+++ b/includes/specials/SpecialUserlogout.php
@@ -0,0 +1,23 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * constructor
+ */
+function wfSpecialUserlogout() {
+ global $wgUser, $wgOut;
+
+ $oldName = $wgUser->getName();
+ $wgUser->logout();
+ $wgOut->setRobotpolicy( 'noindex,nofollow' );
+
+ // Hook.
+ $injected_html = '';
+ wfRunHooks( 'UserLogoutComplete', array(&$wgUser, &$injected_html, $oldName) );
+
+ $wgOut->addHTML( wfMsgExt( 'logouttext', array( 'parse' ) ) . $injected_html );
+ $wgOut->returnToMain();
+}
diff --git a/includes/specials/SpecialUserrights.php b/includes/specials/SpecialUserrights.php
new file mode 100644
index 00000000..fd3c690b
--- /dev/null
+++ b/includes/specials/SpecialUserrights.php
@@ -0,0 +1,589 @@
+<?php
+/**
+ * Special page to allow managing user group membership
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * A class to manage user levels rights.
+ * @ingroup SpecialPage
+ */
+class UserrightsPage extends SpecialPage {
+ # The target of the local right-adjuster's interest. Can be gotten from
+ # either a GET parameter or a subpage-style parameter, so have a member
+ # variable for it.
+ protected $mTarget;
+ protected $isself = false;
+
+ public function __construct() {
+ parent::__construct( 'Userrights' );
+ }
+
+ public function isRestricted() {
+ return true;
+ }
+
+ public function userCanExecute( $user ) {
+ $available = $this->changeableGroups();
+ return !empty( $available['add'] )
+ or !empty( $available['remove'] )
+ or ($this->isself and
+ (!empty( $available['add-self'] )
+ or !empty( $available['remove-self'] )));
+ }
+
+ /**
+ * Manage forms to be shown according to posted data.
+ * Depending on the submit button used, call a form or a save function.
+ *
+ * @param $par Mixed: string if any subpage provided, else null
+ */
+ function execute( $par ) {
+ // If the visitor doesn't have permissions to assign or remove
+ // any groups, it's a bit silly to give them the user search prompt.
+ global $wgUser, $wgRequest;
+
+ if( $par ) {
+ $this->mTarget = $par;
+ } else {
+ $this->mTarget = $wgRequest->getVal( 'user' );
+ }
+
+ if (!$this->mTarget) {
+ /*
+ * If the user specified no target, and they can only
+ * edit their own groups, automatically set them as the
+ * target.
+ */
+ $available = $this->changeableGroups();
+ if (empty($available['add']) && empty($available['remove']))
+ $this->mTarget = $wgUser->getName();
+ }
+
+ if ($this->mTarget == $wgUser->getName())
+ $this->isself = true;
+
+ if( !$this->userCanExecute( $wgUser ) ) {
+ // fixme... there may be intermediate groups we can mention.
+ global $wgOut;
+ $wgOut->showPermissionsErrorPage( array(
+ $wgUser->isAnon()
+ ? 'userrights-nologin'
+ : 'userrights-notallowed' ) );
+ return;
+ }
+
+ if ( wfReadOnly() ) {
+ global $wgOut;
+ $wgOut->readOnlyPage();
+ return;
+ }
+
+ $this->outputHeader();
+
+ $this->setHeaders();
+
+ // show the general form
+ $this->switchForm();
+
+ if( $wgRequest->wasPosted() ) {
+ // save settings
+ if( $wgRequest->getCheck( 'saveusergroups' ) ) {
+ $reason = $wgRequest->getVal( 'user-reason' );
+ if( $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ), $this->mTarget ) ) {
+ $this->saveUserGroups(
+ $this->mTarget,
+ $reason
+ );
+ }
+ }
+ }
+
+ // show some more forms
+ if( $this->mTarget ) {
+ $this->editUserGroupsForm( $this->mTarget );
+ }
+ }
+
+ /**
+ * Save user groups changes in the database.
+ * Data comes from the editUserGroupsForm() form function
+ *
+ * @param $username String: username to apply changes to.
+ * @param $reason String: reason for group change
+ * @return null
+ */
+ function saveUserGroups( $username, $reason = '') {
+ global $wgRequest, $wgUser, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
+
+ $user = $this->fetchUser( $username );
+ if( !$user ) {
+ return;
+ }
+
+ $allgroups = $this->getAllGroups();
+ $addgroup = array();
+ $removegroup = array();
+
+ // This could possibly create a highly unlikely race condition if permissions are changed between
+ // when the form is loaded and when the form is saved. Ignoring it for the moment.
+ foreach ($allgroups as $group) {
+ // We'll tell it to remove all unchecked groups, and add all checked groups.
+ // Later on, this gets filtered for what can actually be removed
+ if ($wgRequest->getCheck( "wpGroup-$group" )) {
+ $addgroup[] = $group;
+ } else {
+ $removegroup[] = $group;
+ }
+ }
+
+ // 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'];
+ }
+
+ $removegroup = array_unique(
+ array_intersect( (array)$removegroup, $removable ) );
+ $addgroup = array_unique(
+ array_intersect( (array)$addgroup, $addable ) );
+
+ $oldGroups = $user->getGroups();
+ $newGroups = $oldGroups;
+ // remove then add groups
+ if( $removegroup ) {
+ $newGroups = array_diff($newGroups, $removegroup);
+ foreach( $removegroup as $group ) {
+ $user->removeGroup( $group );
+ }
+ }
+ if( $addgroup ) {
+ $newGroups = array_merge($newGroups, $addgroup);
+ foreach( $addgroup as $group ) {
+ $user->addGroup( $group );
+ }
+ }
+ $newGroups = array_unique( $newGroups );
+
+ // Ensure that caches are cleared
+ $user->invalidateCache();
+
+ wfDebug( 'oldGroups: ' . print_r( $oldGroups, true ) );
+ wfDebug( 'newGroups: ' . print_r( $newGroups, true ) );
+ if( $user instanceof User ) {
+ // hmmm
+ wfRunHooks( 'UserRights', array( &$user, $addgroup, $removegroup ) );
+ }
+
+ if( $newGroups != $oldGroups ) {
+ $this->addLogEntry( $user, $oldGroups, $newGroups );
+ }
+ }
+
+ /**
+ * Add a rights log entry for an action.
+ */
+ function addLogEntry( $user, $oldGroups, $newGroups ) {
+ global $wgRequest;
+ $log = new LogPage( 'rights' );
+
+ $log->addEntry( 'rights',
+ $user->getUserPage(),
+ $wgRequest->getText( 'user-reason' ),
+ array(
+ $this->makeGroupNameListForLog( $oldGroups ),
+ $this->makeGroupNameListForLog( $newGroups )
+ )
+ );
+ }
+
+ /**
+ * Edit user groups membership
+ * @param $username String: name of the user.
+ */
+ function editUserGroupsForm( $username ) {
+ global $wgOut;
+
+ $user = $this->fetchUser( $username );
+ if( !$user ) {
+ return;
+ }
+
+ $groups = $user->getGroups();
+
+ $this->showEditUserGroupsForm( $user, $groups );
+
+ // This isn't really ideal logging behavior, but let's not hide the
+ // interwiki logs if we're using them as is.
+ $this->showLogFragment( $user, $wgOut );
+ }
+
+ /**
+ * Normalize the input username, which may be local or remote, and
+ * return a user (or proxy) object for manipulating it.
+ *
+ * Side effects: error output for invalid access
+ * @return mixed User, UserRightsProxy, or null
+ */
+ function fetchUser( $username ) {
+ global $wgOut, $wgUser;
+
+ $parts = explode( '@', $username );
+ if( count( $parts ) < 2 ) {
+ $name = trim( $username );
+ $database = '';
+ } else {
+ list( $name, $database ) = array_map( 'trim', $parts );
+
+ if( !$wgUser->isAllowed( 'userrights-interwiki' ) ) {
+ $wgOut->addWikiMsg( 'userrights-no-interwiki' );
+ return null;
+ }
+ if( !UserRightsProxy::validDatabase( $database ) ) {
+ $wgOut->addWikiMsg( 'userrights-nodatabase', $database );
+ return null;
+ }
+ }
+
+ if( $name == '' ) {
+ $wgOut->addWikiMsg( 'nouserspecified' );
+ return false;
+ }
+
+ if( $name{0} == '#' ) {
+ // Numeric ID can be specified...
+ // We'll do a lookup for the name internally.
+ $id = intval( substr( $name, 1 ) );
+
+ if( $database == '' ) {
+ $name = User::whoIs( $id );
+ } else {
+ $name = UserRightsProxy::whoIs( $database, $id );
+ }
+
+ if( !$name ) {
+ $wgOut->addWikiMsg( 'noname' );
+ return null;
+ }
+ }
+
+ if( $database == '' ) {
+ $user = User::newFromName( $name );
+ } else {
+ $user = UserRightsProxy::newFromName( $database, $name );
+ }
+
+ if( !$user || $user->isAnon() ) {
+ $wgOut->addWikiMsg( 'nosuchusershort', $username );
+ return null;
+ }
+
+ return $user;
+ }
+
+ function makeGroupNameList( $ids ) {
+ if( empty( $ids ) ) {
+ return wfMsg( 'rightsnone' );
+ } else {
+ return implode( ', ', $ids );
+ }
+ }
+
+ function makeGroupNameListForLog( $ids ) {
+ if( empty( $ids ) ) {
+ return '';
+ } else {
+ return $this->makeGroupNameList( $ids );
+ }
+ }
+
+ /**
+ * Output a form to allow searching for a user
+ */
+ function switchForm() {
+ global $wgOut, $wgScript;
+ $wgOut->addHTML(
+ Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'name' => 'uluser', 'id' => 'mw-userrights-form1' ) ) .
+ Xml::hidden( 'title', $this->getTitle()->getPrefixedText() ) .
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', array(), wfMsg( 'userrights-lookup-user' ) ) .
+ Xml::inputLabel( wfMsg( 'userrights-user-editname' ), 'user', 'username', 30, $this->mTarget ) . ' ' .
+ Xml::submitButton( wfMsg( 'editusergroup' ) ) .
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' ) . "\n"
+ );
+ }
+
+ /**
+ * Go through used and available groups and return the ones that this
+ * form will be able to manipulate based on the current user's system
+ * permissions.
+ *
+ * @param $groups Array: list of groups the given user is in
+ * @return Array: Tuple of addable, then removable groups
+ */
+ protected function splitGroups( $groups ) {
+ global $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
+ list($addable, $removable) = array_values( $this->changeableGroups() );
+
+ $removable = array_intersect(
+ array_merge($this->isself ? $wgGroupsRemoveFromSelf : array(), $removable),
+ $groups ); // Can't remove groups the user doesn't have
+ $addable = array_diff(
+ array_merge($this->isself ? $wgGroupsAddToSelf : array(), $addable),
+ $groups ); // Can't add groups the user does have
+
+ return array( $addable, $removable );
+ }
+
+ /**
+ * Show the form to edit group memberships.
+ *
+ * @param $user User or UserRightsProxy you're editing
+ * @param $groups Array: Array of groups the user is in
+ */
+ protected function showEditUserGroupsForm( $user, $groups ) {
+ global $wgOut, $wgUser, $wgLang;
+
+ list( $addable, $removable ) = $this->splitGroups( $groups );
+
+ $list = array();
+ foreach( $user->getGroups() as $group )
+ $list[] = self::buildGroupLink( $group );
+
+ $grouplist = '';
+ if( count( $list ) > 0 ) {
+ $grouplist = wfMsgHtml( 'userrights-groupsmember' );
+ $grouplist = '<p>' . $grouplist . ' ' . $wgLang->listToText( $list ) . '</p>';
+ }
+ $wgOut->addHTML(
+ Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getTitle()->getLocalURL(), 'name' => 'editGroup', 'id' => 'mw-userrights-form2' ) ) .
+ Xml::hidden( 'user', $this->mTarget ) .
+ Xml::hidden( 'wpEditToken', $wgUser->editToken( $this->mTarget ) ) .
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', array(), wfMsg( 'userrights-editusergroup' ) ) .
+ wfMsgExt( 'editinguser', array( 'parse' ), wfEscapeWikiText( $user->getName() ) ) .
+ wfMsgExt( 'userrights-groups-help', array( 'parse' ) ) .
+ $grouplist .
+ Xml::tags( 'p', null, $this->groupCheckboxes( $groups ) ) .
+ Xml::openElement( 'table', array( 'border' => '0', 'id' => 'mw-userrights-table-outer' ) ) .
+ "<tr>
+ <td class='mw-label'>" .
+ Xml::label( wfMsg( 'userrights-reason' ), 'wpReason' ) .
+ "</td>
+ <td class='mw-input'>" .
+ Xml::input( 'user-reason', 60, false, array( 'id' => 'wpReason', 'maxlength' => 255 ) ) .
+ "</td>
+ </tr>
+ <tr>
+ <td></td>
+ <td class='mw-submit'>" .
+ Xml::submitButton( wfMsg( 'saveusergroups' ), array( 'name' => 'saveusergroups' ) ) .
+ "</td>
+ </tr>" .
+ Xml::closeElement( 'table' ) . "\n" .
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' ) . "\n"
+ );
+ }
+
+ /**
+ * Format a link to a group description page
+ *
+ * @param $group string
+ * @return string
+ */
+ private static function buildGroupLink( $group ) {
+ static $cache = array();
+ if( !isset( $cache[$group] ) )
+ $cache[$group] = User::makeGroupLinkHtml( $group, User::getGroupName( $group ) );
+ return $cache[$group];
+ }
+
+ /**
+ * Returns an array of all groups that may be edited
+ * @return array Array of groups that may be edited.
+ */
+ protected static function getAllGroups() {
+ return User::getAllGroups();
+ }
+
+ /**
+ * Adds a table with checkboxes where you can select what groups to add/remove
+ *
+ * @param $usergroups Array: groups the user belongs to
+ * @return string XHTML table element with checkboxes
+ */
+ private function groupCheckboxes( $usergroups ) {
+ $allgroups = $this->getAllGroups();
+ $ret = '';
+
+ $column = 1;
+ $settable_col = '';
+ $unsettable_col = '';
+
+ foreach ($allgroups as $group) {
+ $set = in_array( $group, $usergroups );
+ # Should the checkbox be disabled?
+ $disabled = !(
+ ( $set && $this->canRemove( $group ) ) ||
+ ( !$set && $this->canAdd( $group ) ) );
+ # Do we need to point out that this action is irreversible?
+ $irreversible = !$disabled && (
+ ($set && !$this->canAdd( $group )) ||
+ (!$set && !$this->canRemove( $group ) ) );
+
+ $attr = $disabled ? array( 'disabled' => 'disabled' ) : array();
+ $text = $irreversible
+ ? wfMsgHtml( 'userrights-irreversible-marker', User::getGroupMember( $group ) )
+ : User::getGroupMember( $group );
+ $checkbox = Xml::checkLabel( $text, "wpGroup-$group",
+ "wpGroup-$group", $set, $attr );
+ $checkbox = $disabled ? Xml::tags( 'span', array( 'class' => 'mw-userrights-disabled' ), $checkbox ) : $checkbox;
+
+ if ($disabled) {
+ $unsettable_col .= "$checkbox<br />\n";
+ } else {
+ $settable_col .= "$checkbox<br />\n";
+ }
+ }
+
+ if ($column) {
+ $ret .= Xml::openElement( 'table', array( 'border' => '0', 'class' => 'mw-userrights-groups' ) ) .
+ "<tr>
+";
+ if( $settable_col !== '' ) {
+ $ret .= xml::element( 'th', null, wfMsg( 'userrights-changeable-col' ) );
+ }
+ if( $unsettable_col !== '' ) {
+ $ret .= xml::element( 'th', null, wfMsg( 'userrights-unchangeable-col' ) );
+ }
+ $ret.= "</tr>
+ <tr>
+";
+ if( $settable_col !== '' ) {
+ $ret .=
+" <td style='vertical-align:top;'>
+ $settable_col
+ </td>
+";
+ }
+ if( $unsettable_col !== '' ) {
+ $ret .=
+" <td style='vertical-align:top;'>
+ $unsettable_col
+ </td>
+";
+ }
+ $ret .= Xml::closeElement( 'tr' ) . Xml::closeElement( 'table' );
+ }
+
+ return $ret;
+ }
+
+ /**
+ * @param $group String: the name of the group to check
+ * @return bool Can we remove the group?
+ */
+ private function canRemove( $group ) {
+ // $this->changeableGroups()['remove'] doesn't work, of course. Thanks,
+ // PHP.
+ $groups = $this->changeableGroups();
+ return in_array( $group, $groups['remove'] ) || ($this->isself && in_array( $group, $groups['remove-self'] ));
+ }
+
+ /**
+ * @param $group string: the name of the group to check
+ * @return bool Can we add the group?
+ */
+ private function canAdd( $group ) {
+ $groups = $this->changeableGroups();
+ return in_array( $group, $groups['add'] ) || ($this->isself && in_array( $group, $groups['add-self'] ));
+ }
+
+ /**
+ * Returns an array of the groups that the user can add/remove.
+ *
+ * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) )
+ */
+ function changeableGroups() {
+ global $wgUser, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
+
+ if( $wgUser->isAllowed( 'userrights' ) ) {
+ // This group gives the right to modify everything (reverse-
+ // compatibility with old "userrights lets you change
+ // everything")
+ // Using array_merge to make the groups reindexed
+ $all = array_merge( User::getAllGroups() );
+ return array(
+ 'add' => $all,
+ 'remove' => $all,
+ 'add-self' => array(),
+ 'remove-self' => array()
+ );
+ }
+
+ // Okay, it's not so simple, we will have to go through the arrays
+ $groups = array(
+ 'add' => array(),
+ 'remove' => array(),
+ 'add-self' => $wgGroupsAddToSelf,
+ 'remove-self' => $wgGroupsRemoveFromSelf);
+ $addergroups = $wgUser->getEffectiveGroups();
+
+ foreach ($addergroups as $addergroup) {
+ $groups = array_merge_recursive(
+ $groups, $this->changeableByGroup($addergroup)
+ );
+ $groups['add'] = array_unique( $groups['add'] );
+ $groups['remove'] = array_unique( $groups['remove'] );
+ }
+ return $groups;
+ }
+
+ /**
+ * Returns an array of the groups that a particular group can add/remove.
+ *
+ * @param $group String: the group to check for whether it can add/remove
+ * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) )
+ */
+ private function changeableByGroup( $group ) {
+ global $wgAddGroups, $wgRemoveGroups;
+
+ $groups = array( 'add' => array(), 'remove' => array() );
+ if( empty($wgAddGroups[$group]) ) {
+ // Don't add anything to $groups
+ } elseif( $wgAddGroups[$group] === true ) {
+ // You get everything
+ $groups['add'] = User::getAllGroups();
+ } elseif( is_array($wgAddGroups[$group]) ) {
+ $groups['add'] = $wgAddGroups[$group];
+ }
+
+ // Same thing for remove
+ if( empty($wgRemoveGroups[$group]) ) {
+ } elseif($wgRemoveGroups[$group] === true ) {
+ $groups['remove'] = User::getAllGroups();
+ } elseif( is_array($wgRemoveGroups[$group]) ) {
+ $groups['remove'] = $wgRemoveGroups[$group];
+ }
+ return $groups;
+ }
+
+ /**
+ * Show a rights log fragment for the specified user
+ *
+ * @param $user User to show log for
+ * @param $output OutputPage to use
+ */
+ protected function showLogFragment( $user, $output ) {
+ $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
new file mode 100644
index 00000000..8c8e386d
--- /dev/null
+++ b/includes/specials/SpecialVersion.php
@@ -0,0 +1,391 @@
+<?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 {
+ private $firstExtOpened = true;
+
+ /**
+ * main()
+ */
+ function execute() {
+ global $wgOut, $wgMessageCache, $wgSpecialVersionShowHooks;
+ $wgMessageCache->loadAllMessages();
+
+ $wgOut->addHTML( '<div dir="ltr">' );
+ $text =
+ $this->MediaWikiCredits() .
+ $this->softwareInformation() .
+ $this->extensionCredits();
+ if ( $wgSpecialVersionShowHooks ) {
+ $text .= $this->wgHooks();
+ }
+ $wgOut->addWikiText( $text );
+ $wgOut->addHTML( $this->IPInfo() );
+ $wgOut->addHTML( '</div>' );
+ }
+
+ /**#@+
+ * @private
+ */
+
+ /**
+ * @return wiki text showing the license information
+ */
+ static function MediaWikiCredits() {
+ $ret = Xml::element( 'h2', array( 'id' => 'mw-version-license' ), wfMsg( 'version-license' ) ) .
+ "__NOTOC__
+ This wiki is powered by '''[http://www.mediawiki.org/ MediaWiki]''',
+ copyright (C) 2001-2008 Magnus Manske, Brion Vibber, Lee Daniel Crocker,
+ Tim Starling, Erik Möller, Gabriel Wicke, Ævar Arnfjörð Bjarmason,
+ Niklas Laxström, Domas Mituzas, Rob Church, Yuri Astrakhan, Aryeh Gregor,
+ Aaron Schulz and others.
+
+ MediaWiki 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.
+
+ MediaWiki 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 [{{SERVER}}{{SCRIPTPATH}}/COPYING 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
+ or [http://www.gnu.org/licenses/old-licenses/gpl-2.0.html read it online].
+ ";
+
+ return str_replace( "\t\t", '', $ret ) . "\n";
+ }
+
+ /**
+ * @return wiki text showing the third party software versions (apache, php, mysql).
+ */
+ static function softwareInformation() {
+ $dbr = wfGetDB( DB_SLAVE );
+
+ return Xml::element( 'h2', array( 'id' => 'mw-version-software' ), wfMsg( 'version-software' ) ) .
+ Xml::openElement( 'table', array( 'id' => 'sv-software' ) ) .
+ "<tr>
+ <th>" . wfMsg( 'version-software-product' ) . "</th>
+ <th>" . wfMsg( 'version-software-version' ) . "</th>
+ </tr>\n
+ <tr>
+ <td>[http://www.mediawiki.org/ MediaWiki]</td>
+ <td>" . self::getVersionLinked() . "</td>
+ </tr>\n
+ <tr>
+ <td>[http://www.php.net/ PHP]</td>
+ <td>" . phpversion() . " (" . php_sapi_name() . ")</td>
+ </tr>\n
+ <tr>
+ <td>" . $dbr->getSoftwareLink() . "</td>
+ <td>" . $dbr->getServerVersion() . "</td>
+ </tr>\n" .
+ Xml::closeElement( 'table' );
+ }
+
+ /**
+ * Return a string of the MediaWiki version with SVN revision if available
+ *
+ * @return mixed
+ */
+ public static function getVersion() {
+ global $wgVersion, $IP;
+ wfProfileIn( __METHOD__ );
+ $svn = self::getSvnRevision( $IP );
+ $version = $svn ? "$wgVersion (r$svn)" : $wgVersion;
+ wfProfileOut( __METHOD__ );
+ return $version;
+ }
+
+ /**
+ * Return a string of the MediaWiki version with a link to SVN revision if
+ * available
+ *
+ * @return mixed
+ */
+ public static function getVersionLinked() {
+ global $wgVersion, $IP;
+ wfProfileIn( __METHOD__ );
+ $svn = self::getSvnRevision( $IP );
+ $viewvc = 'http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/?pathrev=';
+ $version = $svn ? "$wgVersion ([{$viewvc}{$svn} r$svn])" : $wgVersion;
+ wfProfileOut( __METHOD__ );
+ return $version;
+ }
+
+ /** Generate wikitext showing extensions name, URL, author and description */
+ function extensionCredits() {
+ global $wgExtensionCredits, $wgExtensionFunctions, $wgParser, $wgSkinExtensionFunctions;
+
+ if ( ! count( $wgExtensionCredits ) && ! count( $wgExtensionFunctions ) && ! count( $wgSkinExtensionFunctions ) )
+ return '';
+
+ $extensionTypes = array(
+ 'specialpage' => wfMsg( 'version-specialpages' ),
+ 'parserhook' => wfMsg( 'version-parserhooks' ),
+ 'variable' => wfMsg( 'version-variables' ),
+ 'media' => wfMsg( 'version-mediahandlers' ),
+ 'other' => wfMsg( 'version-other' ),
+ );
+ wfRunHooks( 'SpecialVersionExtensionTypes', array( &$this, &$extensionTypes ) );
+
+ $out = Xml::element( 'h2', array( 'id' => 'mw-version-ext' ), wfMsg( 'version-extensions' ) ) .
+ Xml::openElement( 'table', array( 'id' => 'sv-ext' ) );
+
+ foreach ( $extensionTypes as $type => $text ) {
+ if ( isset ( $wgExtensionCredits[$type] ) && count ( $wgExtensionCredits[$type] ) ) {
+ $out .= $this->openExtType( $text );
+
+ usort( $wgExtensionCredits[$type], array( $this, 'compare' ) );
+
+ foreach ( $wgExtensionCredits[$type] as $extension ) {
+ if ( isset( $extension['version'] ) ) {
+ $version = $extension['version'];
+ } elseif ( isset( $extension['svn-revision'] ) &&
+ preg_match( '/\$(?:Rev|LastChangedRevision|Revision): *(\d+)/',
+ $extension['svn-revision'], $m ) )
+ {
+ $version = 'r' . $m[1];
+ } else {
+ $version = null;
+ }
+
+ $out .= $this->formatCredits(
+ isset ( $extension['name'] ) ? $extension['name'] : '',
+ $version,
+ isset ( $extension['author'] ) ? $extension['author'] : '',
+ isset ( $extension['url'] ) ? $extension['url'] : null,
+ isset ( $extension['description'] ) ? $extension['description'] : '',
+ isset ( $extension['descriptionmsg'] ) ? $extension['descriptionmsg'] : ''
+ );
+ }
+ }
+ }
+
+ if ( count( $wgExtensionFunctions ) ) {
+ $out .= $this->openExtType( wfMsg( 'version-extension-functions' ) );
+ $out .= '<tr><td colspan="3">' . $this->listToText( $wgExtensionFunctions ) . "</td></tr>\n";
+ }
+
+ if ( $cnt = count( $tags = $wgParser->getTags() ) ) {
+ for ( $i = 0; $i < $cnt; ++$i )
+ $tags[$i] = "&lt;{$tags[$i]}&gt;";
+ $out .= $this->openExtType( wfMsg( 'version-parser-extensiontags' ) );
+ $out .= '<tr><td colspan="3">' . $this->listToText( $tags ). "</td></tr>\n";
+ }
+
+ if( $cnt = count( $fhooks = $wgParser->getFunctionHooks() ) ) {
+ $out .= $this->openExtType( wfMsg( 'version-parser-function-hooks' ) );
+ $out .= '<tr><td colspan="3">' . $this->listToText( $fhooks ) . "</td></tr>\n";
+ }
+
+ if ( count( $wgSkinExtensionFunctions ) ) {
+ $out .= $this->openExtType( wfMsg( 'version-skin-extension-functions' ) );
+ $out .= '<tr><td colspan="3">' . $this->listToText( $wgSkinExtensionFunctions ) . "</td></tr>\n";
+ }
+ $out .= Xml::closeElement( 'table' );
+ return $out;
+ }
+
+ /** Callback to sort extensions by type */
+ function compare( $a, $b ) {
+ global $wgLang;
+ if( $a['name'] === $b['name'] ) {
+ return 0;
+ } else {
+ return $wgLang->lc( $a['name'] ) > $wgLang->lc( $b['name'] )
+ ? 1
+ : -1;
+ }
+ }
+
+ function formatCredits( $name, $version = null, $author = null, $url = null, $description = null, $descriptionMsg = null ) {
+ $extension = isset( $url ) ? "[$url $name]" : $name;
+ $version = isset( $version ) ? "(" . wfMsg( 'version-version' ) . " $version)" : '';
+
+ # Look for a localized description
+ if( isset( $descriptionMsg ) ) {
+ $msg = wfMsg( $descriptionMsg );
+ if ( !wfEmptyMsg( $descriptionMsg, $msg ) && $msg != '' ) {
+ $description = $msg;
+ }
+ }
+
+ return "<tr>
+ <td><em>$extension $version</em></td>
+ <td>$description</td>
+ <td>" . $this->listToText( (array)$author ) . "</td>
+ </tr>\n";
+ }
+
+ /**
+ * @return string
+ */
+ function wgHooks() {
+ global $wgHooks;
+
+ if ( count( $wgHooks ) ) {
+ $myWgHooks = $wgHooks;
+ ksort( $myWgHooks );
+
+ $ret = Xml::element( 'h2', array( 'id' => 'mw-version-hooks' ), wfMsg( 'version-hooks' ) ) .
+ Xml::openElement( 'table', array( 'id' => 'sv-hooks' ) ) .
+ "<tr>
+ <th>" . wfMsg( 'version-hook-name' ) . "</th>
+ <th>" . wfMsg( 'version-hook-subscribedby' ) . "</th>
+ </tr>\n";
+
+ foreach ( $myWgHooks as $hook => $hooks )
+ $ret .= "<tr>
+ <td>$hook</td>
+ <td>" . $this->listToText( $hooks ) . "</td>
+ </tr>\n";
+
+ $ret .= Xml::closeElement( 'table' );
+ return $ret;
+ } else
+ return '';
+ }
+
+ private function openExtType($text, $name = null) {
+ $opt = array( 'colspan' => 3 );
+ $out = '';
+
+ if(!$this->firstExtOpened) {
+ // Insert a spacing line
+ $out .= '<tr class="sv-space">' . Xml::element( 'td', $opt ) . "</tr>\n";
+ }
+ $this->firstExtOpened = false;
+
+ if($name) { $opt['id'] = "sv-$name"; }
+
+ $out .= "<tr>" . Xml::element( 'th', $opt, $text) . "</tr>\n";
+ return $out;
+ }
+
+ /**
+ * @static
+ *
+ * @return string
+ */
+ function IPInfo() {
+ $ip = str_replace( '--', ' - ', htmlspecialchars( wfGetIP() ) );
+ return "<!-- visited from $ip -->\n" .
+ "<span style='display:none'>visited from $ip</span>";
+ }
+
+ /**
+ * @param array $list
+ * @return string
+ */
+ function listToText( $list ) {
+ $cnt = count( $list );
+
+ if ( $cnt == 1 ) {
+ // Enforce always returning a string
+ return (string)$this->arrayToString( $list[0] );
+ } elseif ( $cnt == 0 ) {
+ return '';
+ } else {
+ 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";
+ }
+ }
+
+ /**
+ * @static
+ *
+ * @param mixed $list Will convert an array to string if given and return
+ * the paramater unaltered otherwise
+ * @return mixed
+ */
+ function arrayToString( $list ) {
+ if( is_object( $list ) ) {
+ $class = get_class( $list );
+ return "($class)";
+ } elseif ( ! is_array( $list ) ) {
+ return $list;
+ } else {
+ $class = get_class( $list[0] );
+ return "($class, {$list[1]})";
+ }
+ }
+
+ /**
+ * Retrieve the revision number of a Subversion working directory.
+ *
+ * @param string $dir
+ * @return mixed revision number as int, or false if not a SVN checkout
+ */
+ public static function getSvnRevision( $dir ) {
+ // http://svnbook.red-bean.com/nightly/en/svn.developer.insidewc.html
+ $entries = $dir . '/.svn/entries';
+
+ if( !file_exists( $entries ) ) {
+ return false;
+ }
+
+ $content = file( $entries );
+
+ // check if file is xml (subversion release <= 1.3) or not (subversion release = 1.4)
+ if( preg_match( '/^<\?xml/', $content[0] ) ) {
+ // subversion is release <= 1.3
+ if( !function_exists( 'simplexml_load_file' ) ) {
+ // We could fall back to expat... YUCK
+ return false;
+ }
+
+ // SimpleXml whines about the xmlns...
+ wfSuppressWarnings();
+ $xml = simplexml_load_file( $entries );
+ wfRestoreWarnings();
+
+ if( $xml ) {
+ foreach( $xml->entry as $entry ) {
+ if( $xml->entry[0]['name'] == '' ) {
+ // The directory entry should always have a revision marker.
+ if( $entry['revision'] ) {
+ return intval( $entry['revision'] );
+ }
+ }
+ }
+ }
+ return false;
+ } else {
+ // subversion is release 1.4
+ return intval( $content[3] );
+ }
+ }
+
+ /**#@-*/
+}
+
+/**#@-*/
diff --git a/includes/specials/SpecialWantedcategories.php b/includes/specials/SpecialWantedcategories.php
new file mode 100644
index 00000000..7497f9be
--- /dev/null
+++ b/includes/specials/SpecialWantedcategories.php
@@ -0,0 +1,90 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * A querypage to list the most wanted categories - implements Special:Wantedcategories
+ *
+ * @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
+ */
+class WantedCategoriesPage extends QueryPage {
+
+ function getName() {
+ return 'Wantedcategories';
+ }
+
+ function isExpensive() {
+ return true;
+ }
+
+ function isSyndicated() {
+ return false;
+ }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ list( $categorylinks, $page ) = $dbr->tableNamesN( 'categorylinks', 'page' );
+ $name = $dbr->addQuotes( $this->getName() );
+ return
+ "
+ SELECT
+ $name as type,
+ " . NS_CATEGORY . " as namespace,
+ cl_to as title,
+ COUNT(*) as value
+ FROM $categorylinks
+ LEFT JOIN $page ON cl_to = page_title AND page_namespace = ". NS_CATEGORY ."
+ WHERE page_title IS NULL
+ GROUP BY cl_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 wfSpecialWantedCategories() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $wpp = new WantedCategoriesPage();
+
+ $wpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialWantedpages.php b/includes/specials/SpecialWantedpages.php
new file mode 100644
index 00000000..10133409
--- /dev/null
+++ b/includes/specials/SpecialWantedpages.php
@@ -0,0 +1,131 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * implements Special:Wantedpages
+ * @ingroup SpecialPage
+ */
+class WantedPagesPage extends QueryPage {
+ var $nlinks;
+
+ function WantedPagesPage( $inc = false, $nlinks = true ) {
+ $this->setListoutput( $inc );
+ $this->nlinks = $nlinks;
+ }
+
+ function getName() {
+ return 'Wantedpages';
+ }
+
+ function isExpensive() {
+ return true;
+ }
+ function isSyndicated() { return false; }
+
+ function getSQL() {
+ global $wgWantedPagesThreshold;
+ $count = $wgWantedPagesThreshold - 1;
+ $dbr = wfGetDB( DB_SLAVE );
+ $pagelinks = $dbr->tableName( 'pagelinks' );
+ $page = $dbr->tableName( 'page' );
+ return
+ "SELECT 'Wantedpages' AS type,
+ pl_namespace AS namespace,
+ pl_title AS title,
+ COUNT(*) AS value
+ FROM $pagelinks
+ LEFT JOIN $page AS pg1
+ ON pl_namespace = pg1.page_namespace AND pl_title = pg1.page_title
+ LEFT JOIN $page AS pg2
+ ON pl_from = pg2.page_id
+ WHERE pg1.page_namespace IS NULL
+ AND pl_namespace NOT IN ( 2, 3 )
+ AND pg2.page_namespace != 8
+ GROUP BY pl_namespace, pl_title
+ HAVING COUNT(*) > $count";
+ }
+
+ /**
+ * Cache page existence for performance
+ */
+ 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 );
+ }
+
+ /**
+ * Format an individual result
+ *
+ * @param $skin Skin to use for UI elements
+ * @param $result Result row
+ * @return string
+ */
+ public function formatResult( $skin, $result ) {
+ $title = Title::makeTitleSafe( $result->namespace, $result->title );
+ if( $title instanceof Title ) {
+ if( $this->isCached() ) {
+ $pageLink = $title->exists()
+ ? '<s>' . $skin->makeLinkObj( $title ) . '</s>'
+ : $skin->makeBrokenLinkObj( $title );
+ } else {
+ $pageLink = $skin->makeBrokenLinkObj( $title );
+ }
+ return wfSpecialList( $pageLink, $this->makeWlhLink( $title, $skin, $result ) );
+ } else {
+ $tsafe = htmlspecialchars( $result->title );
+ return "Invalid title in result set; {$tsafe}";
+ }
+ }
+
+ /**
+ * Make a "what links here" link for a specified result if required
+ *
+ * @param $title Title to make the link for
+ * @param $skin Skin to use
+ * @param $result Result row
+ * @return string
+ */
+ private function makeWlhLink( $title, $skin, $result ) {
+ global $wgLang;
+ if( $this->nlinks ) {
+ $wlh = SpecialPage::getTitleFor( 'Whatlinkshere' );
+ $label = wfMsgExt( 'nlinks', array( 'parsemag', 'escape' ),
+ $wgLang->formatNum( $result->value ) );
+ return $skin->makeKnownLinkObj( $wlh, $label, 'target=' . $title->getPrefixedUrl() );
+ } else {
+ return null;
+ }
+ }
+
+}
+
+/**
+ * constructor
+ */
+function wfSpecialWantedpages( $par = null, $specialPage ) {
+ $inc = $specialPage->including();
+
+ if ( $inc ) {
+ @list( $limit, $nlinks ) = explode( '/', $par, 2 );
+ $limit = (int)$limit;
+ $nlinks = $nlinks === 'nlinks';
+ $offset = 0;
+ } else {
+ list( $limit, $offset ) = wfCheckLimits();
+ $nlinks = true;
+ }
+
+ $wpp = new WantedPagesPage( $inc, $nlinks );
+
+ $wpp->doQuery( $offset, $limit, !$inc );
+}
diff --git a/includes/specials/SpecialWatchlist.php b/includes/specials/SpecialWatchlist.php
new file mode 100644
index 00000000..db7cd423
--- /dev/null
+++ b/includes/specials/SpecialWatchlist.php
@@ -0,0 +1,383 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage Watchlist
+ */
+
+/**
+ * Constructor
+ *
+ * @param $par Parameter passed to the page
+ */
+function wfSpecialWatchlist( $par ) {
+ global $wgUser, $wgOut, $wgLang, $wgRequest;
+ global $wgRCShowWatchingUsers, $wgEnotifWatchlist, $wgShowUpdatedMarker;
+ global $wgEnotifWatchlist;
+ $fname = 'wfSpecialWatchlist';
+
+ $skin = $wgUser->getSkin();
+ $specialTitle = SpecialPage::getTitleFor( 'Watchlist' );
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
+
+ # 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 ) );
+ return;
+ }
+
+ $wgOut->setPageTitle( wfMsg( 'watchlist' ) );
+
+ $sub = wfMsgExt( 'watchlistfor', 'parseinline', $wgUser->getName() );
+ $sub .= '<br />' . WatchlistEditor::buildTools( $wgUser->getSkin() );
+ $wgOut->setSubtitle( $sub );
+
+ if( ( $mode = WatchlistEditor::getMode( $wgRequest, $par ) ) !== false ) {
+ $editor = new WatchlistEditor();
+ $editor->execute( $wgUser, $wgOut, $wgRequest, $mode );
+ return;
+ }
+
+ $uid = $wgUser->getId();
+ 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' ),
+ /* ? */ 'namespace' => 'all',
+ );
+
+ 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['hideminor'] = $wgUser->getBoolOption( 'watchlisthideminor' );
+
+ # Get query variables
+ $days = $wgRequest->getVal( 'days', $prefs['days'] );
+ $hideOwn = $wgRequest->getBool( 'hideOwn', $prefs['hideown'] );
+ $hideBots = $wgRequest->getBool( 'hideBots', $prefs['hidebots'] );
+ $hideMinor = $wgRequest->getBool( 'hideMinor', $prefs['hideminor'] );
+
+ # Get namespace value, if supplied, and prepare a WHERE fragment
+ $nameSpace = $wgRequest->getIntOrNull( 'namespace' );
+ if( !is_null( $nameSpace ) ) {
+ $nameSpace = intval( $nameSpace );
+ $nameSpaceClause = " AND rc_namespace = $nameSpace";
+ } else {
+ $nameSpace = '';
+ $nameSpaceClause = '';
+ }
+
+ $dbr = wfGetDB( DB_SLAVE, 'watchlist' );
+ list( $page, $watchlist, $recentchanges ) = $dbr->tableNamesN( 'page', 'watchlist', 'recentchanges' );
+
+ $watchlistCount = $dbr->selectField( 'watchlist', 'COUNT(*)',
+ array( 'wl_user' => $uid ), __METHOD__ );
+ // Adjust for page X, talk:page X, which are both stored separately,
+ // but treated together
+ $nitems = floor($watchlistCount / 2);
+
+ if( is_null($days) || !is_numeric($days) ) {
+ $big = 1000; /* The magical big */
+ if($nitems > $big) {
+ # Set default cutoff shorter
+ $days = $defaults['days'] = (12.0 / 24.0); # 12 hours...
+ } else {
+ $days = $defaults['days']; # default cutoff for shortlisters
+ }
+ } else {
+ $days = floatval($days);
+ }
+
+ // Dump everything here
+ $nondefaults = array();
+
+ wfAppendToArrayIfNotDefault('days' , $days , $defaults, $nondefaults);
+ wfAppendToArrayIfNotDefault('hideOwn' , (int)$hideOwn , $defaults, $nondefaults);
+ wfAppendToArrayIfNotDefault('hideBots' , (int)$hideBots, $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) {
+ $wgOut->addWikiMsg( 'nowatchlist' );
+ return;
+ }
+
+ 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;
+ */
+ }
+
+ # If the watchlist is relatively short, it's simplest to zip
+ # down its entirety and then sort the results.
+
+ # If it's relatively long, it may be worth our while to zip
+ # through the time-sorted page list checking for watched items.
+
+ # 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)
+ if( $wgUser->getOption( 'extendwatchlist' )) {
+ $andLatest='';
+ $limitWatchlist = 'LIMIT ' . intval( $wgUser->getOption( 'wllimit' ) );
+ } else {
+ # Top log Ids for a page are not stored
+ $andLatest = 'AND (rc_this_oldid=page_latest OR rc_type=' . RC_LOG . ') ';
+ $limitWatchlist = '';
+ }
+
+ $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" );
+ }
+ if ( $wgShowUpdatedMarker ) {
+ $wltsfield = ", ${watchlist}.wl_notificationtimestamp ";
+ } else {
+ $wltsfield = '';
+ }
+ $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 );
+ $numRows = $dbr->numRows( $res );
+
+ /* Start bottom header */
+ $wgOut->addHTML( "<hr />\n" );
+
+ if($days >= 1) {
+ $wgOut->addHTML(
+ 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',
+ $wgLang->formatNum( $numRows ),
+ $wgLang->formatNum( round($days*24) )
+ ) . '<br />'
+ );
+ }
+
+ $wgOut->addHTML( "\n" . wlCutoffLinks( $days, 'Watchlist', $nondefaults ) . "<br />\n" );
+
+ # Spit out some control panel links
+ $thisTitle = SpecialPage::getTitleFor( 'Watchlist' );
+ $skin = $wgUser->getSkin();
+
+ # Hide/show bot edits
+ $label = $hideBots ? wfMsgHtml( 'watchlist-show-bots' ) : wfMsgHtml( 'watchlist-hide-bots' );
+ $linkBits = wfArrayToCGI( array( 'hideBots' => 1 - (int)$hideBots ), $nondefaults );
+ $links[] = $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits );
+
+ # Hide/show own edits
+ $label = $hideOwn ? wfMsgHtml( 'watchlist-show-own' ) : wfMsgHtml( 'watchlist-hide-own' );
+ $linkBits = wfArrayToCGI( array( 'hideOwn' => 1 - (int)$hideOwn ), $nondefaults );
+ $links[] = $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 ) );
+
+ # Form for namespace filtering
+ $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $thisTitle->getLocalUrl() ) );
+ $form .= '<p>';
+ $form .= Xml::label( wfMsg( 'namespace' ), 'namespace' ) . '&nbsp;';
+ $form .= Xml::namespaceSelector( $nameSpace, '' ) . '&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 );
+ $form .= Xml::closeElement( 'form' );
+ $wgOut->addHtml( $form );
+
+ # If there's nothing to show, stop here
+ if( $numRows == 0 ) {
+ $wgOut->addWikiMsg( 'watchnochange' );
+ return;
+ }
+
+ /* End bottom header */
+
+ /* Do link batch query */
+ $linkBatch = new LinkBatch;
+ while ( $row = $dbr->fetchObject( $res ) ) {
+ $userNameUnderscored = str_replace( ' ', '_', $row->rc_user_text );
+ if ( $row->rc_user != 0 ) {
+ $linkBatch->add( NS_USER, $userNameUnderscored );
+ }
+ $linkBatch->add( NS_USER_TALK, $userNameUnderscored );
+ }
+ $linkBatch->execute();
+ $dbr->dataSeek( $res, 0 );
+
+ $list = ChangesList::newFromUser( $wgUser );
+
+ $s = $list->beginRecentChangesList();
+ $counter = 1;
+ while ( $obj = $dbr->fetchObject( $res ) ) {
+ # Make RC entry
+ $rc = RecentChange::newFromRow( $obj );
+ $rc->counter = $counter++;
+
+ if ( $wgShowUpdatedMarker ) {
+ $updated = $obj->wl_notificationtimestamp;
+ } else {
+ $updated = false;
+ }
+
+ if ($wgRCShowWatchingUsers && $wgUser->getOption( 'shownumberswatching' )) {
+ $rc->numberofWatchingusers = $dbr->selectField( 'watchlist',
+ 'COUNT(*)',
+ array(
+ 'wl_namespace' => $obj->rc_namespace,
+ 'wl_title' => $obj->rc_title,
+ ),
+ __METHOD__ );
+ } else {
+ $rc->numberofWatchingusers = 0;
+ }
+
+ $s .= $list->recentChangesLine( $rc, $updated );
+ }
+ $s .= $list->endRecentChangesList();
+
+ $dbr->freeResult( $res );
+ $wgOut->addHTML( $s );
+
+}
+
+function wlHoursLink( $h, $page, $options = array() ) {
+ global $wgUser, $wgLang, $wgContLang;
+ $sk = $wgUser->getSkin();
+ $s = $sk->makeKnownLink(
+ $wgContLang->specialPage( $page ),
+ $wgLang->formatNum( $h ),
+ wfArrayToCGI( array('days' => ($h / 24.0)), $options ) );
+ return $s;
+}
+
+function wlDaysLink( $d, $page, $options = array() ) {
+ global $wgUser, $wgLang, $wgContLang;
+ $sk = $wgUser->getSkin();
+ $s = $sk->makeKnownLink(
+ $wgContLang->specialPage( $page ),
+ ($d ? $wgLang->formatNum( $d ) : wfMsgHtml( 'watchlistall2' ) ),
+ wfArrayToCGI( array('days' => $d), $options ) );
+ return $s;
+}
+
+/**
+ * Returns html
+ */
+function wlCutoffLinks( $days, $page = 'Watchlist', $options = array() ) {
+ $hours = array( 1, 2, 6, 12 );
+ $days = array( 1, 3, 7 );
+ $i = 0;
+ foreach( $hours as $h ) {
+ $hours[$i++] = wlHoursLink( $h, $page, $options );
+ }
+ $i = 0;
+ foreach( $days as $d ) {
+ $days[$i++] = wlDaysLink( $d, $page, $options );
+ }
+ return wfMsgExt('wlshowlast',
+ array('parseinline', 'replaceafter'),
+ implode(' | ', $hours),
+ implode(' | ', $days),
+ wlDaysLink( 0, $page, $options ) );
+}
+
+/**
+ * Count the number of items on a user's watchlist
+ *
+ * @param $talk Include talk pages
+ * @return integer
+ */
+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' );
+ $row = $dbr->fetchObject( $res );
+ $count = $row->count;
+ $dbr->freeResult( $res );
+
+ # Halve to remove talk pages if needed
+ if( !$talk )
+ $count = floor( $count / 2 );
+
+ return( $count );
+}
diff --git a/includes/specials/SpecialWhatlinkshere.php b/includes/specials/SpecialWhatlinkshere.php
new file mode 100644
index 00000000..3502e33c
--- /dev/null
+++ b/includes/specials/SpecialWhatlinkshere.php
@@ -0,0 +1,408 @@
+<?php
+/**
+ * @todo Use some variant of Pager or something; the pagination here is lousy.
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Entry point
+ * @param $par String: An article name ??
+ */
+function wfSpecialWhatlinkshere($par = NULL) {
+ global $wgRequest;
+ $page = new WhatLinksHerePage( $wgRequest, $par );
+ $page->execute();
+}
+
+/**
+ * implements Special:Whatlinkshere
+ * @ingroup SpecialPage
+ */
+class WhatLinksHerePage {
+ // Stored data
+ protected $par;
+
+ // Stored objects
+ protected $opts, $target, $selfTitle;
+
+ // Stored globals
+ protected $skin, $request;
+
+ protected $limits = array( 20, 50, 100, 250, 500 );
+
+ function WhatLinksHerePage( $request, $par = null ) {
+ global $wgUser;
+ $this->request = $request;
+ $this->skin = $wgUser->getSkin();
+ $this->par = $par;
+ }
+
+ function execute() {
+ global $wgOut;
+
+ $opts = new FormOptions();
+
+ $opts->add( 'target', '' );
+ $opts->add( 'namespace', '', FormOptions::INTNULL );
+ $opts->add( 'limit', 50 );
+ $opts->add( 'from', 0 );
+ $opts->add( 'back', 0 );
+ $opts->add( 'hideredirs', false );
+ $opts->add( 'hidetrans', false );
+ $opts->add( 'hidelinks', false );
+ $opts->add( 'hideimages', false );
+
+ $opts->fetchValuesFromRequest( $this->request );
+ $opts->validateIntBounds( 'limit', 0, 5000 );
+
+ // Give precedence to subpage syntax
+ if ( isset($this->par) ) {
+ $opts->setValue( 'target', $this->par );
+ }
+
+ // Bind to member variable
+ $this->opts = $opts;
+
+ $this->target = Title::newFromURL( $opts->getValue( 'target' ) );
+ if( !$this->target ) {
+ $wgOut->addHTML( $this->whatlinkshereForm() );
+ return;
+ }
+
+ $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");
+
+ $this->showIndirectLinks( 0, $this->target, $opts->getValue( 'limit' ),
+ $opts->getValue( 'from' ), $opts->getValue( 'back' ) );
+ }
+
+ /**
+ * @param $level int Recursion level
+ * @param $target Title Target title
+ * @param $limit int Number of entries to display
+ * @param $from Title Display from this article ID
+ * @param $back Title Display from this article ID at backwards scrolling
+ * @private
+ */
+ function showIndirectLinks( $level, $target, $limit, $from = 0, $back = 0 ) {
+ global $wgOut, $wgMaxRedirectLinksRetrieved;
+ $dbr = wfGetDB( DB_SLAVE );
+ $options = array();
+
+ $hidelinks = $this->opts->getValue( 'hidelinks' );
+ $hideredirs = $this->opts->getValue( 'hideredirs' );
+ $hidetrans = $this->opts->getValue( 'hidetrans' );
+ $hideimages = $target->getNamespace() != NS_IMAGE || $this->opts->getValue( 'hideimages' );
+
+ $fetchlinks = (!$hidelinks || !$hideredirs);
+
+ // Make the query
+ $plConds = array(
+ 'page_id=pl_from',
+ 'pl_namespace' => $target->getNamespace(),
+ 'pl_title' => $target->getDBkey(),
+ );
+ if( $hideredirs ) {
+ $plConds['page_is_redirect'] = 0;
+ } elseif( $hidelinks ) {
+ $plConds['page_is_redirect'] = 1;
+ }
+
+ $tlConds = array(
+ 'page_id=tl_from',
+ 'tl_namespace' => $target->getNamespace(),
+ 'tl_title' => $target->getDBkey(),
+ );
+
+ $ilConds = array(
+ 'page_id=il_from',
+ 'il_to' => $target->getDBkey(),
+ );
+
+ $namespace = $this->opts->getValue( 'namespace' );
+ if ( is_int($namespace) ) {
+ $plConds['page_namespace'] = $namespace;
+ $tlConds['page_namespace'] = $namespace;
+ $ilConds['page_namespace'] = $namespace;
+ }
+
+ if ( $from ) {
+ $tlConds[] = "tl_from >= $from";
+ $plConds[] = "pl_from >= $from";
+ $ilConds[] = "il_from >= $from";
+ }
+
+ // Read an extra row as an at-end check
+ $queryLimit = $limit + 1;
+
+ // Enforce join order, sometimes namespace selector may
+ // trigger filesorts which are far less efficient than scanning many entries
+ $options[] = 'STRAIGHT_JOIN';
+
+ $options['LIMIT'] = $queryLimit;
+ $fields = array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' );
+
+ if( $fetchlinks ) {
+ $options['ORDER BY'] = 'pl_from';
+ $plRes = $dbr->select( array( 'pagelinks', 'page' ), $fields,
+ $plConds, __METHOD__, $options );
+ }
+
+ if( !$hidetrans ) {
+ $options['ORDER BY'] = 'tl_from';
+ $tlRes = $dbr->select( array( 'templatelinks', 'page' ), $fields,
+ $tlConds, __METHOD__, $options );
+ }
+
+ if( !$hideimages ) {
+ $options['ORDER BY'] = 'il_from';
+ $ilRes = $dbr->select( array( 'imagelinks', 'page' ), $fields,
+ $ilConds, __METHOD__, $options );
+ }
+
+ 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() );
+ }
+ return;
+ }
+
+ // Read the rows into an array and remove duplicates
+ // templatelinks comes second so that the templatelinks row overwrites the
+ // pagelinks row, so we get (inclusion) rather than nothing
+ if( $fetchlinks ) {
+ while ( $row = $dbr->fetchObject( $plRes ) ) {
+ $row->is_template = 0;
+ $row->is_image = 0;
+ $rows[$row->page_id] = $row;
+ }
+ $dbr->freeResult( $plRes );
+
+ }
+ if( !$hidetrans ) {
+ while ( $row = $dbr->fetchObject( $tlRes ) ) {
+ $row->is_template = 1;
+ $row->is_image = 0;
+ $rows[$row->page_id] = $row;
+ }
+ $dbr->freeResult( $tlRes );
+ }
+ if( !$hideimages ) {
+ while ( $row = $dbr->fetchObject( $ilRes ) ) {
+ $row->is_template = 0;
+ $row->is_image = 1;
+ $rows[$row->page_id] = $row;
+ }
+ $dbr->freeResult( $ilRes );
+ }
+
+ // Sort by key and then change the keys to 0-based indices
+ ksort( $rows );
+ $rows = array_values( $rows );
+
+ $numRows = count( $rows );
+
+ // Work out the start and end IDs, for prev/next links
+ if ( $numRows > $limit ) {
+ // More rows available after these ones
+ // Get the ID from the last row in the result set
+ $nextId = $rows[$limit]->page_id;
+ // Remove undisplayed rows
+ $rows = array_slice( $rows, 0, $limit );
+ } else {
+ // No more rows after
+ $nextId = false;
+ }
+ $prevId = $from;
+
+ if ( $level == 0 ) {
+ $wgOut->addHTML( $this->whatlinkshereForm() );
+ $wgOut->addHTML( $this->getFilterPanel() );
+ $wgOut->addWikiMsg( 'linkshere', $this->target->getPrefixedText() );
+
+ $prevnext = $this->getPrevNext( $prevId, $nextId );
+ $wgOut->addHTML( $prevnext );
+ }
+
+ $wgOut->addHTML( $this->listStart() );
+ foreach ( $rows as $row ) {
+ $nt = Title::makeTitle( $row->page_namespace, $row->page_title );
+
+ if ( $row->page_is_redirect && $level < 2 ) {
+ $wgOut->addHTML( $this->listItem( $row, $nt, true ) );
+ $this->showIndirectLinks( $level + 1, $nt, $wgMaxRedirectLinksRetrieved );
+ $wgOut->addHTML( Xml::closeElement( 'li' ) );
+ } else {
+ $wgOut->addHTML( $this->listItem( $row, $nt ) );
+ }
+ }
+
+ $wgOut->addHTML( $this->listEnd() );
+
+ if( $level == 0 ) {
+ $wgOut->addHTML( $prevnext );
+ }
+ }
+
+ protected function listStart() {
+ return Xml::openElement( 'ul' );
+ }
+
+ protected function listItem( $row, $nt, $notClose = false ) {
+ # local message cache
+ static $msgcache = null;
+ if ( $msgcache === null ) {
+ static $msgs = array( 'isredirect', 'istemplate', 'semicolon-separator',
+ 'whatlinkshere-links', 'isimage' );
+ $msgcache = array();
+ foreach ( $msgs as $msg ) {
+ $msgcache[$msg] = wfMsgHtml( $msg );
+ }
+ }
+
+ $suppressRedirect = $row->page_is_redirect ? 'redirect=no' : '';
+ $link = $this->skin->makeKnownLinkObj( $nt, '', $suppressRedirect );
+
+ // Display properties (redirect or template)
+ $propsText = '';
+ $props = array();
+ if ( $row->page_is_redirect )
+ $props[] = $msgcache['isredirect'];
+ if ( $row->is_template )
+ $props[] = $msgcache['istemplate'];
+ if( $row->is_image )
+ $props[] = $msgcache['isimage'];
+
+ if ( count( $props ) ) {
+ $propsText = '(' . implode( $msgcache['semicolon-separator'], $props ) . ')';
+ }
+
+ # Space for utilities links, with a what-links-here link provided
+ $wlhLink = $this->wlhLink( $nt, $msgcache['whatlinkshere-links'] );
+ $wlh = Xml::wrapClass( "($wlhLink)", 'mw-whatlinkshere-tools' );
+
+ return $notClose ?
+ Xml::openElement( 'li' ) . "$link $propsText $wlh\n" :
+ Xml::tags( 'li', null, "$link $propsText $wlh" ) . "\n";
+ }
+
+ protected function listEnd() {
+ return Xml::closeElement( 'ul' );
+ }
+
+ protected function wlhLink( Title $target, $text ) {
+ static $title = null;
+ if ( $title === null )
+ $title = SpecialPage::getTitleFor( 'Whatlinkshere' );
+
+ $targetText = $target->getPrefixedUrl();
+ return $this->skin->makeKnownLinkObj( $title, $text, 'target=' . $targetText );
+ }
+
+ function makeSelfLink( $text, $query ) {
+ return $this->skin->makeKnownLinkObj( $this->selfTitle, $text, $query );
+ }
+
+ function getPrevNext( $prevId, $nextId ) {
+ global $wgLang;
+ $currentLimit = $this->opts->getValue( 'limit' );
+ $fmtLimit = $wgLang->formatNum( $currentLimit );
+ $prev = wfMsgExt( 'whatlinkshere-prev', array( 'parsemag', 'escape' ), $fmtLimit );
+ $next = wfMsgExt( 'whatlinkshere-next', array( 'parsemag', 'escape' ), $fmtLimit );
+
+ $changed = $this->opts->getChangedValues();
+ unset($changed['target']); // Already in the request title
+
+ if ( 0 != $prevId ) {
+ $overrides = array( 'from' => $this->opts->getValue( 'back' ) );
+ $prev = $this->makeSelfLink( $prev, wfArrayToCGI( $overrides, $changed ) );
+ }
+ if ( 0 != $nextId ) {
+ $overrides = array( 'from' => $nextId, 'back' => $prevId );
+ $next = $this->makeSelfLink( $next, wfArrayToCGI( $overrides, $changed ) );
+ }
+
+ $limitLinks = array();
+ foreach ( $this->limits as $limit ) {
+ $prettyLimit = $wgLang->formatNum( $limit );
+ $overrides = array( 'limit' => $limit );
+ $limitLinks[] = $this->makeSelfLink( $prettyLimit, wfArrayToCGI( $overrides, $changed ) );
+ }
+
+ $nums = implode ( ' | ', $limitLinks );
+
+ return wfMsgHtml( 'viewprevnext', $prev, $next, $nums );
+ }
+
+ function whatlinkshereForm() {
+ global $wgScript, $wgTitle;
+
+ // We get nicer value from the title object
+ $this->opts->consumeValue( 'target' );
+ // Reset these for new requests
+ $this->opts->consumeValues( array( 'back', 'from' ) );
+
+ $target = $this->target ? $this->target->getPrefixedText() : '';
+ $namespace = $this->opts->consumeValue( 'namespace' );
+
+ # Build up the form
+ $f = Xml::openElement( 'form', array( 'action' => $wgScript ) );
+
+ # Values that should not be forgotten
+ $f .= Xml::hidden( 'title', $wgTitle->getPrefixedText() );
+ foreach ( $this->opts->getUnconsumedValues() as $name => $value ) {
+ $f .= Xml::hidden( $name, $value );
+ }
+
+ $f .= Xml::fieldset( wfMsg( 'whatlinkshere' ) );
+
+ # Target input
+ $f .= Xml::inputLabel( wfMsg( 'whatlinkshere-page' ), 'target',
+ 'mw-whatlinkshere-target', 40, $target );
+
+ $f .= ' ';
+
+ # Namespace selector
+ $f .= Xml::label( wfMsg( 'namespace' ), 'namespace' ) . '&nbsp;' .
+ Xml::namespaceSelector( $namespace, '' );
+
+ # Submit
+ $f .= Xml::submitButton( wfMsg( 'allpagessubmit' ) );
+
+ # Close
+ $f .= Xml::closeElement( 'fieldset' ) . Xml::closeElement( 'form' ) . "\n";
+
+ return $f;
+ }
+
+ function getFilterPanel() {
+ $show = wfMsgHtml( 'show' );
+ $hide = wfMsgHtml( 'hide' );
+
+ $changed = $this->opts->getChangedValues();
+ unset($changed['target']); // Already in the request title
+
+ $links = array();
+ $types = array( 'hidetrans', 'hidelinks', 'hideredirs' );
+ if( $this->target->getNamespace() == NS_IMAGE )
+ $types[] = 'hideimages';
+ foreach( $types as $type ) {
+ $chosen = $this->opts->getValue( $type );
+ $msg = wfMsgHtml( "whatlinkshere-{$type}", $chosen ? $show : $hide );
+ $overrides = array( $type => !$chosen );
+ $links[] = $this->makeSelfLink( $msg, wfArrayToCGI( $overrides, $changed ) );
+ }
+ return Xml::fieldset( wfMsg( 'whatlinkshere-filters' ), implode( '&nbsp;|&nbsp;', $links ) );
+ }
+}
diff --git a/includes/specials/SpecialWithoutinterwiki.php b/includes/specials/SpecialWithoutinterwiki.php
new file mode 100644
index 00000000..2092e43b
--- /dev/null
+++ b/includes/specials/SpecialWithoutinterwiki.php
@@ -0,0 +1,88 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Special page lists pages without language links
+ *
+ * @ingroup SpecialPage
+ * @author Rob Church <robchur@gmail.com>
+ */
+class WithoutInterwikiPage extends PageQueryPage {
+ private $prefix = '';
+
+ function getName() {
+ return 'Withoutinterwiki';
+ }
+
+ function getPageHeader() {
+ global $wgScript, $wgMiserMode;
+
+ # Do not show useless input form if wiki is running in misermode
+ if( $wgMiserMode ) {
+ return '';
+ }
+
+ $prefix = $this->prefix;
+ $t = SpecialPage::getTitleFor( $this->getName() );
+
+ return Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) .
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', null, wfMsg( 'withoutinterwiki-legend' ) ) .
+ Xml::hidden( 'title', $t->getPrefixedText() ) .
+ Xml::inputLabel( wfMsg( 'allpagesprefix' ), 'prefix', 'wiprefix', 20, $prefix ) . ' ' .
+ Xml::submitButton( wfMsg( 'withoutinterwiki-submit' ) ) .
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' );
+ }
+
+ function sortDescending() {
+ return false;
+ }
+
+ function isExpensive() {
+ return true;
+ }
+
+ function isSyndicated() {
+ return false;
+ }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ list( $page, $langlinks ) = $dbr->tableNamesN( 'page', 'langlinks' );
+ $prefix = $this->prefix ? "AND page_title LIKE '" . $dbr->escapeLike( $this->prefix ) . "%'" : '';
+ return
+ "SELECT 'Withoutinterwiki' AS type,
+ page_namespace AS namespace,
+ page_title AS title,
+ page_title AS value
+ FROM $page
+ LEFT JOIN $langlinks
+ ON ll_from = page_id
+ WHERE ll_title IS NULL
+ AND page_namespace=" . NS_MAIN . "
+ AND page_is_redirect = 0
+ {$prefix}";
+ }
+
+ function setPrefix( $prefix = '' ) {
+ $this->prefix = $prefix;
+ }
+
+}
+
+function wfSpecialWithoutinterwiki() {
+ global $wgRequest, $wgContLang, $wgCapitalLinks;
+ list( $limit, $offset ) = wfCheckLimits();
+ if( $wgCapitalLinks ) {
+ $prefix = $wgContLang->ucfirst( $wgRequest->getVal( 'prefix' ) );
+ } else {
+ $prefix = $wgRequest->getVal( 'prefix' );
+ }
+ $wip = new WithoutInterwikiPage();
+ $wip->setPrefix( $prefix );
+ $wip->doQuery( $offset, $limit );
+}
diff --git a/includes/templates/NoLocalSettings.php b/includes/templates/NoLocalSettings.php
index 9020c46e..75a7e95a 100644
--- a/includes/templates/NoLocalSettings.php
+++ b/includes/templates/NoLocalSettings.php
@@ -1,4 +1,9 @@
<?php
+/**
+ * @file
+ * @ingroup Templates
+ */
+
# Prevent XSS
if ( isset( $wgVersion ) ) {
$wgVersion = htmlspecialchars( $wgVersion );
diff --git a/includes/templates/Userlogin.php b/includes/templates/Userlogin.php
index ac24800a..deeeb274 100644
--- a/includes/templates/Userlogin.php
+++ b/includes/templates/Userlogin.php
@@ -1,15 +1,14 @@
<?php
/**
- * @addtogroup Templates
+ * @defgroup Templates Templates
+ * @file
+ * @ingroup Templates
*/
if( !defined( 'MEDIAWIKI' ) ) die( -1 );
-/** */
-require_once( 'includes/SkinTemplate.php' );
-
/**
* HTML template for Special:Userlogin form
- * @addtogroup Templates
+ * @ingroup Templates
*/
class UserloginTemplate extends QuickTemplate {
function execute() {
@@ -95,9 +94,18 @@ class UserloginTemplate extends QuickTemplate {
}
/**
- * @addtogroup Templates
+ * @ingroup Templates
*/
class UsercreateTemplate extends QuickTemplate {
+ function addInputItem( $name, $value, $type, $msg ) {
+ $this->data['extraInput'][] = array(
+ 'name' => $name,
+ 'value' => $value,
+ 'type' => $type,
+ 'msg' => $msg,
+ );
+ }
+
function execute() {
if( $this->data['message'] ) {
?>
@@ -198,15 +206,57 @@ class UsercreateTemplate extends QuickTemplate {
/> <label for="wpRemember"><?php $this->msg('remembermypassword') ?></label>
</td>
</tr>
+<?php
+ $tabIndex = 8;
+ if ( isset( $this->data['extraInput'] ) && is_array( $this->data['extraInput'] ) ) {
+ foreach ( $this->data['extraInput'] as $inputItem ) { ?>
+ <tr>
+ <?php
+ if ( !empty( $inputItem['msg'] ) && $inputItem['type'] != 'checkbox' ) {
+ ?><td class="mw-label"><label for="<?php
+ echo htmlspecialchars( $inputItem['name'] ); ?>"><?php
+ $this->msgWiki( $inputItem['msg'] ) ?></label><?php
+ } else {
+ ?><td><?php
+ }
+ ?></td>
+ <td class="mw-input">
+ <input type="<?php echo htmlspecialchars( $inputItem['type'] ) ?>" name="<?php
+ echo htmlspecialchars( $inputItem['name'] ); ?>"
+ tabindex="<?php echo $tabIndex++; ?>"
+ value="<?php
+ if ( $inputItem['type'] != 'checkbox' ) {
+ echo htmlspecialchars( $inputItem['value'] );
+ } else {
+ echo '1';
+ }
+ ?>" id="<?php echo htmlspecialchars( $inputItem['name'] ); ?>"
+ <?php
+ if ( $inputItem['type'] == 'checkbox' && !empty( $inputItem['value'] ) )
+ echo 'checked="checked"';
+ ?> /> <?php
+ if ( $inputItem['type'] == 'checkbox' && !empty( $inputItem['msg'] ) ) {
+ ?>
+ <label for="<?php echo htmlspecialchars( $inputItem['name'] ); ?>"><?php
+ $this->msg( $inputItem['msg'] ) ?></label><?php
+ }
+ ?>
+ </td>
+ </tr>
+<?php
+
+ }
+ }
+?>
<tr>
<td></td>
<td class="mw-submit">
<input type='submit' name="wpCreateaccount" id="wpCreateaccount"
- tabindex="8"
+ tabindex="<?php echo $tabIndex++; ?>"
value="<?php $this->msg('createaccount') ?>" />
<?php if( $this->data['createemail'] ) { ?>
<input type='submit' name="wpCreateaccountMail" id="wpCreateaccountMail"
- tabindex="9"
+ tabindex="<?php echo $tabIndex++; ?>"
value="<?php $this->msg('createaccountmail') ?>" />
<?php } ?>
</td>
@@ -220,5 +270,3 @@ class UsercreateTemplate extends QuickTemplate {
}
}
-
-?>
diff --git a/includes/tidy.conf b/includes/tidy.conf
index 3cefcf8f..09412f05 100644
--- a/includes/tidy.conf
+++ b/includes/tidy.conf
@@ -4,15 +4,15 @@
show-body-only: yes
force-output: yes
-tidy-mark: no
+tidy-mark: no
wrap: 0
wrap-attributes: no
literal-attributes: yes
-output-xhtml: yes
+output-xhtml: yes
numeric-entities: yes
enclose-text: yes
enclose-block-text: yes
-quiet: yes
+quiet: yes
quote-nbsp: yes
fix-backslash: no
fix-uri: no
diff --git a/includes/zhtable/Makefile b/includes/zhtable/Makefile
index c63e4db7..1407c93c 100644
--- a/includes/zhtable/Makefile
+++ b/includes/zhtable/Makefile
@@ -12,7 +12,7 @@ DIFF = LANG=zh_CN.UTF8 diff
CC ?= gcc
SF_MIRROR = easynews
-SCIM_TABLES_VER = 0.5.7
+SCIM_TABLES_VER = 0.5.8
SCIM_PINYIN_VER = 0.5.91
LIBTABE_VER = 0.2.3
@@ -21,22 +21,40 @@ INSTDIR = /usr/local/share/zhdaemons/
all: ZhConversion.php tradphrases.notsure simpphrases.notsure wordlist toHans.dict toHant.dict toCN.dict toTW.dict toHK.dict toSG.dict
-Unihan.txt:
+# Download Unihan database and Traditional Chinese / Simplified Chinese phrases files
+Unihan.zip:
wget -nc ftp://ftp.unicode.org/Public/UNIDATA/Unihan.zip
- unzip -q Unihan.zip
-EZ.txt.in:
+scim-tables-$(SCIM_TABLES_VER).tar.gz:
wget -nc http://$(SF_MIRROR).dl.sourceforge.net/sourceforge/scim/scim-tables-$(SCIM_TABLES_VER).tar.gz
- tar -xzf scim-tables-$(SCIM_TABLES_VER).tar.gz -O scim-tables-$(SCIM_TABLES_VER)/tables/zh/EZ-Big.txt.in > EZ.txt.in
-phrase_lib.txt:
+scim-pinyin-$(SCIM_PINYIN_VER).tar.gz:
wget -nc http://$(SF_MIRROR).dl.sourceforge.net/sourceforge/scim/scim-pinyin-$(SCIM_PINYIN_VER).tar.gz
+
+libtabe-$(LIBTABE_VER).tgz:
+ wget -nc http://$(SF_MIRROR).dl.sourceforge.net/sourceforge/libtabe/libtabe-$(LIBTABE_VER).tgz
+
+# Extract the file from a comressed files
+Unihan.txt: Unihan.zip
+ unzip -oq Unihan.zip
+
+EZ.txt.in: scim-tables-$(SCIM_TABLES_VER).tar.gz
+ tar -xzf scim-tables-$(SCIM_TABLES_VER).tar.gz -O scim-tables-$(SCIM_TABLES_VER)/tables/zh/EZ-Big.txt.in > EZ.txt.in
+
+Wubi.txt.in: scim-tables-$(SCIM_TABLES_VER).tar.gz
+ tar -xzf scim-tables-$(SCIM_TABLES_VER).tar.gz -O scim-tables-$(SCIM_TABLES_VER)/tables/zh/Wubi.txt.in > Wubi.txt.in
+
+Ziranma.txt.in: scim-tables-$(SCIM_TABLES_VER).tar.gz
+ tar -xzf scim-tables-$(SCIM_TABLES_VER).tar.gz -O scim-tables-$(SCIM_TABLES_VER)/tables/zh/Ziranma.txt.in > Ziranma.txt.in
+
+
+phrase_lib.txt: scim-pinyin-$(SCIM_PINYIN_VER).tar.gz
tar -xzf scim-pinyin-$(SCIM_PINYIN_VER).tar.gz -O scim-pinyin-$(SCIM_PINYIN_VER)/data/phrase_lib.txt > phrase_lib.txt
-tsi.src:
- wget -nc http://$(SF_MIRROR).dl.sourceforge.net/sourceforge/libtabe/libtabe-$(LIBTABE_VER).tgz
+tsi.src: libtabe-$(LIBTABE_VER).tgz
tar -xzf libtabe-$(LIBTABE_VER).tgz -O libtabe/tsi-src/tsi.src > tsi.src
+# Make a word list
wordlist: phrase_lib.txt EZ.txt.in tsi.src
iconv -c -f big5 -t utf8 tsi.src | $(SED) 's/# //g' | $(SED) 's/[ ][0-9].*//' > wordlist
$(SED) 's/\(.*\)\t[0-9][0-9]*.*/\1/' phrase_lib.txt | $(SED) '1,5d' >> wordlist
@@ -64,7 +82,7 @@ simp2trad.t: unihan.s2t.t simp2trad.manual
cat simp2trad.manual tmp1 > simp2trad.t
t2s_1tomany.t: trad2simp.t
- $(GREP) -s ".\{19,\}" trad2simp.t | $(SED) 's/U+...../"/' | $(SED) 's/|U+...../"=>"/' | $(SED) 's/|U+.....//g' | $(SED) 's/|/",/' > t2s_1tomany.t
+ $(GREP) -s ".\{19,\}" trad2simp.t | $(SED) 's/U+...../"/' | $(SED) 's/|U+...../"=>"/' | $(SED) 's/|U+.....//g' | $(SED) 's/|/",/' > t2s_1tomany.t
t2s_1to1.t: trad2simp.t s2t_1tomany.t
$(SED) "/.*|.*|.*|.*/d" trad2simp.t | $(SED) 's/U+[0-9a-z][0-9a-z]*/"/' | $(SED) 's/|U+[0-9a-z][0-9a-z]*/"=>"/' | $(SED) 's/|/",/' > t2s_1to1.t
@@ -97,8 +115,9 @@ tphrase.t: EZ.txt.in tsi.src
iconv -c -f big5 -t utf8 tsi.src | $(SED) 's/ [0-9].*//g' | $(SED) 's/[# ]//g'| $(GREP) "^.\{2,4\}" >> t
sort t | uniq > tphrase.t
-alltradphrases.t: tphrase.t s2t_1tomany.t
+alltradphrases.t: tphrase.t s2t_1tomany.t tradphrases_exclude.manual
for i in `cat s2t_1tomany.t | $(SED) 's/.*=>".//' | $(SED) 's/"//g' |$(SED) 's/,/\n/' | $(SED) 's/\(.\)/\1\n/g' |sort | uniq`; do $(GREP) -s $$i tphrase.t ; done > alltradphrases.t || true
+ cat alltradphrases.t | $(GREP) -vf tradphrases_exclude.manual > alltradphrases.tt ; mv alltradphrases.tt alltradphrases.t
tradphrases_2.t: alltradphrases.t
@@ -123,6 +142,9 @@ tradphrases_4.t: alltradphrases.t
tradphrases.t: tradphrases.manual tradphrases_2.t tradphrases_3.t tradphrases_4.t t2s_1tomany.t
cat tradphrases.manual tradphrases_2.t tradphrases_3.t tradphrases_4.t |sort | uniq > tradphrases.t
for i in `$(SED) 's/"\(.\).*/\1/' t2s_1tomany.t ` ; do $(GREP) $$i tradphrases.t ; done | $(DIFF) tradphrases.t - | $(GREP) '<' | $(SED) 's/< //' > t
+ for i in `$(SED) 's/"\(..\)..*/\1/' t2s_1tomany.t ` ; do $(GREP) $$i tradphrases.t ; done | $(DIFF) tradphrases.t - | $(GREP) '<' | $(SED) 's/< //' >> t
+ mv t tradphrases.t
+ cat tradphrases.t | sort | uniq > t
mv t tradphrases.t
tradphrases.notsure: tradphrases_2.t tradphrases_3.t tradphrases_4.t t2s_1tomany.t
@@ -133,9 +155,19 @@ tradphrases.notsure: tradphrases_2.t tradphrases_3.t tradphrases_4.t t2s_1tomany
ph.t: phrase_lib.txt
$(SED) 's/[\t0-9a-zA-Z]//g' phrase_lib.txt | $(GREP) "^.\{2,4\}$$" > ph.t
-allsimpphrases.t: ph.t
+Wubi.t: Wubi.txt.in
+ $(SED) '1,/BEGIN_TABLE/d' Wubi.txt.in | colrm 1 8 | $(SED) 's/\t.*//' | $(GREP) "^...*" > Wubi.t
+
+Ziranma.t: Ziranma.txt.in
+ $(SED) '1,/BEGIN_TABLE/d' Ziranma.txt.in | colrm 1 8 | $(SED) 's/\t.*//' | $(GREP) "^...*" > Ziranma.t
+
+
+allsimpphrases.t: t2s_1tomany.t ph.t Wubi.t Ziranma.t simpphrases_exclude.manual
rm -f allsimpphrases.t
+ for i in `cat t2s_1tomany.t | $(SED) 's/.*=>".//' | $(SED) 's/"//g' | $(SED) 's/,/\n/' | $(SED) 's/\(.\)/\1\n/g' | sort | uniq `; do $(GREP) $$i Wubi.t >> allsimpphrases.t; done
+ for i in `cat t2s_1tomany.t | $(SED) 's/.*=>".//' | $(SED) 's/"//g' | $(SED) 's/,/\n/' | $(SED) 's/\(.\)/\1\n/g' | sort | uniq `; do $(GREP) $$i Ziranma.t >> allsimpphrases.t; done
for i in `cat t2s_1tomany.t | $(SED) 's/.*=>".//' | $(SED) 's/"//g' | $(SED) 's/,/\n/' | $(SED) 's/\(.\)/\1\n/g' | sort | uniq `; do $(GREP) $$i ph.t >> allsimpphrases.t; done
+ cat allsimpphrases.t | $(GREP) -vf simpphrases_exclude.manual > allsimpphrases.tt ; mv allsimpphrases.tt allsimpphrases.t
simpphrases_2.t: allsimpphrases.t
cat allsimpphrases.t | $(GREP) "^..$$" | sort | uniq > simpphrases_2.t
@@ -153,59 +185,85 @@ simpphrases_4.t: allsimpphrases.t
sort t | uniq > t3
$(DIFF) t3 simpphrases_4.t | $(GREP) ">" | $(SED) 's/> //' > t
mv t simpphrases_4.t
- for i in `cat simpphrases_3.t`; do $(GREP) $$i simpphrases_4.t; done | sort | uniq > t3 || true
+ for i in `cat simpphrases_3.t`; do $(GREP) $$i simpphrases_4.t; done | sort | uniq > t3 || true
$(DIFF) t3 simpphrases_4.t | $(GREP) ">" | $(SED) 's/> //' > t
mv t simpphrases_4.t
-simpphrases.t:simpphrases_2.t simpphrases_3.t simpphrases_4.t t2s_1tomany.t
- cat simpphrases_2.t simpphrases_3.t simpphrases_4.t > simpphrases.t
+simpphrases.t: simpphrases.manual simpphrases_2.t simpphrases_3.t simpphrases_4.t t2s_1tomany.t
+ cat simpphrases.manual simpphrases_2.t simpphrases_3.t simpphrases_4.t > simpphrases.t
for i in `$(SED) 's/"\(.\).*/\1/' t2s_1tomany.t ` ; do $(GREP) $$i simpphrases.t ; done | $(DIFF) simpphrases.t - | $(GREP) '<' | $(SED) 's/< //' > t
+ for i in `$(SED) 's/"\(..\)..*/\1/' t2s_1tomany.t ` ; do $(GREP) $$i simpphrases.t ; done | $(DIFF) simpphrases.t - | $(GREP) '<' | $(SED) 's/< //' >> t
+ mv t simpphrases.t
+ cat simpphrases.t | sort | uniq > t
mv t simpphrases.t
-
-simpphrases.notsure:simpphrases_2.t simpphrases_3.t simpphrases_4.t t2s_1tomany.t
+simpphrases.notsure: simpphrases_2.t simpphrases_3.t simpphrases_4.t t2s_1tomany.t
cat simpphrases_2.t simpphrases_3.t simpphrases_4.t > t
for i in `$(SED) 's/"\(.\).*/\1/' t2s_1tomany.t ` ; do $(GREP) $$i t ; done | $(DIFF) t - | $(GREP) '>' | $(SED) 's/> //' > simpphrases.notsure
-trad2simp1to1.t: t2s_1tomany.t t2s_1to1.t
- $(SED) 's/\(.......\).*/\1",/' t2s_1tomany.t > trad2simp1to1.t
+trad2simp1to1.t: t2s_1tomany.t t2s_1to1.t trad2simp_noconvert.manual
+ $(SED) 's/\(.......\).*/\1",/' t2s_1tomany.t > tt
+ colrm 1 7 < trad2simp.manual | colrm 3 > trad2simpcharsrc.t
+ colrm 1 17 < trad2simp.manual | colrm 3 > trad2simpchardest.t
+ cat trad2simpcharsrc.t | $(GREP) -f trad2simpchardest.t > trad2simprepeatedchar.t
+ cat tt | $(GREP) -vf trad2simprepeatedchar.t > trad2simp1to1.t
cat t2s_1to1.t >> trad2simp1to1.t
-
-simp2trad1to1.t: s2t_1tomany.t s2t_1to1.t
- $(SED) 's/\(.......\).*/\1",/' s2t_1tomany.t > simp2trad1to1.t
+ cat trad2simp1to1.t | $(GREP) -vf trad2simp_noconvert.manual > tt
+ mv tt trad2simp1to1.t
+
+simp2trad1to1.t: s2t_1tomany.t s2t_1to1.t simp2trad.manual simp2trad_noconvert.manual
+ $(SED) 's/\(.......\).*/\1",/' s2t_1tomany.t > tt
+ colrm 1 7 < simp2trad.manual | colrm 3 > simp2tradcharsrc.t
+ colrm 1 17 < simp2trad.manual | colrm 3 > simp2tradchardest.t
+ cat simp2tradcharsrc.t | $(GREP) -f simp2tradchardest.t > simp2tradrepeatedchar.t
+ cat tt | $(GREP) -vf simp2tradrepeatedchar.t > simp2trad1to1.t
cat s2t_1to1.t >> simp2trad1to1.t
+ cat simp2trad1to1.t | $(GREP) -vf simp2trad_noconvert.manual > tt
+ mv tt simp2trad1to1.t
-trad2simp.php: trad2simp1to1.t tradphrases.t
+trad2simp.php: trad2simp1to1.t tradphrases.t trad2simp_supp_unset.manual trad2simp_supp_set.manual
printf '<?php\n$$trad2simp=array(' > trad2simp.php
cat trad2simp1to1.t >> trad2simp.php
+ $(SED) 's/\(.*\)\t\(.*\)/"\1" => "\2",/' trad2simp_supp_set.manual >> trad2simp.php
printf ');\n$$str=\n"' >> trad2simp.php
cat tradphrases.t >> trad2simp.php
printf '";\n$$t=strtr($$str, $$trad2simp);\necho $$t;\n?>' >> trad2simp.php
+ cat trad2simp1to1.t | $(GREP) -vf trad2simp_supp_unset.manual > tt
+ mv tt trad2simp1to1.t
-simp2trad.php: simp2trad1to1.t simpphrases.t
+simp2trad.php: simp2trad1to1.t simpphrases.t simp2trad_supp_set.manual
printf '<?php\n$$simp2trad=array(' > simp2trad.php
cat simp2trad1to1.t >> simp2trad.php
+ $(SED) 's/\(.*\)\t\(.*\)/"\1" => "\2",/' simp2trad_supp_set.manual >> simp2trad.php
printf ');\n$$str=\n"' >> simp2trad.php
cat simpphrases.t >> simp2trad.php
printf '";\n$$t=strtr($$str, $$simp2trad);\necho $$t;\n?>' >> simp2trad.php
-simp2trad.phrases.t: trad2simp.php tradphrases.t
+simp2trad.phrases.t: trad2simp.php tradphrases.t simp2trad_supp_set.manual
php -f trad2simp.php | $(SED) 's/\(.*\)/"\1" => /' > tmp1
cat tradphrases.t | $(SED) 's/\(.*\)/"\1",/' > tmp2
paste tmp1 tmp2 > simp2trad.phrases.t
+ colrm 3 < simp2trad_supp_set.manual > simp2trad_supp_noconvert.t
+ cat trad2simp.php | $(GREP) -vf simp2trad_supp_noconvert.t > trad2simp.tt
+ mv trad2simp.tt trad2simp.php
-trad2simp.phrases.t: simp2trad.php simpphrases.t
+trad2simp.phrases.t: simp2trad.php simpphrases.t trad2simp_supp_set.manual
php -f simp2trad.php | $(SED) 's/\(.*\)/"\1" => /' > tmp1
cat simpphrases.t | $(SED) 's/\(.*\)/"\1",/' > tmp2
paste tmp1 tmp2 > trad2simp.phrases.t
+ colrm 3 < trad2simp_supp_set.manual > trad2simp_supp_noconvert.t
+ cat simp2trad.php | $(GREP) -vf trad2simp_supp_noconvert.t > simp2trad.tt
+ mv simp2trad.tt simp2trad.php
-toHans.dict: trad2simp1to1.t trad2simp.phrases.t
+toHans.dict: trad2simp1to1.t trad2simp.phrases.t toSimp.manual
cat trad2simp1to1.t | $(SED) 's/[, \t]//g' | $(SED) 's/=>/\t/' > toHans.dict
cat trad2simp.phrases.t | $(SED) 's/[, \t]//g' | $(SED) 's/=>/\t/' >> toHans.dict
+ cat toSimp.manual | $(SED) 's/ //g' | $(SED) 's/\(^.*\)\t\(.*\)/"\1"\t"\2"/' >> toHans.dict
-toHant.dict: simp2trad1to1.t simp2trad.phrases.t
+toHant.dict: simp2trad1to1.t simp2trad.phrases.t toTrad.manual
cat simp2trad1to1.t | $(SED) 's/[, \t]//g' | $(SED) 's/=>/\t/' > toHant.dict
cat simp2trad.phrases.t | $(SED) 's/[, \t]//g' | $(SED) 's/=>/\t/' >> toHant.dict
+ cat toTrad.manual | $(SED) 's/ //g' | $(SED) 's/\(^.*\)\t\(.*\)/"\1"\t"\2"/' >> toHant.dict
toTW.dict: toTW.manual
cat toTW.manual | $(SED) 's/ //g' | $(SED) 's/\(^.*\)\t\(.*\)/"\1"\t"\2"/' > toTW.dict
@@ -219,7 +277,7 @@ toCN.dict: toCN.manual
toSG.dict: toSG.manual
cat toSG.manual | $(SED) 's/ //g' | $(SED) 's/\(^.*\)\t\(.*\)/"\1"\t"\2"/' > toSG.dict
-ZhConversion.php: simp2trad1to1.t simp2trad.phrases.t trad2simp1to1.t trad2simp.phrases.t toCN.manual toHK.manual toSG.manual toTW.manual
+ZhConversion.php: simp2trad1to1.t simp2trad.phrases.t trad2simp1to1.t trad2simp.phrases.t toSimp.manual toTrad.manual toCN.manual toHK.manual toSG.manual toTW.manual
printf '<?php\n/**\n * Simplified / Traditional Chinese conversion tables\n' > ZhConversion.php
printf ' *\n * Automatically generated using code and data in includes/zhtable/\n' >> ZhConversion.php
printf ' * Do not modify directly!\n */\n\n' >> ZhConversion.php
@@ -227,12 +285,14 @@ ZhConversion.php: simp2trad1to1.t simp2trad.phrases.t trad2simp1to1.t trad2simp.
cat simp2trad1to1.t >> ZhConversion.php
echo >> ZhConversion.php
cat simp2trad.phrases.t >> ZhConversion.php
+ $(SED) 's/\(.*\)\t\(.*\)/"\1" => "\2",/' toTrad.manual >> ZhConversion.php
echo ');' >> ZhConversion.php
echo >> ZhConversion.php
printf '$$zh2Hans = array(\n' >> ZhConversion.php
cat trad2simp1to1.t >> ZhConversion.php
echo >> ZhConversion.php
cat trad2simp.phrases.t >> ZhConversion.php
+ $(SED) 's/\(.*\)\t\(.*\)/"\1" => "\2",/' toSimp.manual >> ZhConversion.php
echo ');' >> ZhConversion.php
echo >> ZhConversion.php
printf '$$zh2TW = array(\n' >> ZhConversion.php
@@ -259,6 +319,8 @@ cleantmp:
rm -f \
Unihan.txt \
EZ.txt.in \
+ Wubi.txt.in \
+ Ziranma.txt.in \
phrase_lib.txt \
tsi.src
# Temporary files and other trash
diff --git a/includes/zhtable/README b/includes/zhtable/README
index 94dd341d..7e3f87e2 100644
--- a/includes/zhtable/README
+++ b/includes/zhtable/README
@@ -7,10 +7,27 @@ unihan database, and phrases not included in the SCIM package.
冯寿忠,“非对称繁简字”对照表, 《语文建设通讯》1997-9第53期.
/http://www.yywzw.com/jt/feng/fengb01.htm
+- trad2simp.manual: Traditional to Simplified character mapping.
+
+- simp2trad_noconvert.manual: Do not convert the chars as inapporiate.
+
+- trad2simp_noconvert.manual: Do not convert the chars as inapporiate.
+
- tradphrases.manual: Phrases in Traditional Chinese. A portition is obtained
from the TongWen package (http://tongwen.mozdev.org/)
-- toTW.manual, toCN.manual, toSG.manual and toHK.manual: special phrase
+- simpphrases.manual: Phrases in Simplified Chinese.
+
+- tradphrases_exclude.manual: Excluding several phrases from
+ the SCIM phrases as inappoiated.
+
+- simpphrases_exclude.manual: Excluding several phrases from
+ the SCIM phrases as inapporated.
+
+- toTrad.manual, toSimp.manual: Special phrase mappings that
+ tradphrases.manual or simphrases.manual cannot be handled.
+
+- toTW.manual, toCN.manual, toSG.manual and toHK.manual: Special phrase
mappings.
-zhengzhu at gmail.com \ No newline at end of file
+zhengzhu at gmail dot com & shinjiman at gmail dot com
diff --git a/includes/zhtable/simp2trad.manual b/includes/zhtable/simp2trad.manual
index b5e1c3ae..140495f9 100644
--- a/includes/zhtable/simp2trad.manual
+++ b/includes/zhtable/simp2trad.manual
@@ -2,18 +2,18 @@ U+0753b画|U+0756b畫|U+07575畵|
U+0677f板|U+0677f板|U+095c6闆|
U+08868表|U+08868表|U+09336錶|
U+0624d才|U+0624d才|U+07e94纔|
-U+04e11丑|U+0919c醜|U+04e11丑|
+U+04e11丑|U+04e11丑|U+0919c醜|
U+051fa出|U+051fa出|U+09f63齣|
-U+06dc0淀|U+06fb1澱|U+06dc0淀|
+U+06dc0淀|U+06dc0淀|U+06fb1澱|
U+051ac冬|U+051ac冬|U+09f15鼕|
-U+08303范|U+07bc4範|U+08303范|
+U+08303范|U+08303范|U+07bc4範|
U+04e30丰|U+08c50豐|U+04e30丰|
U+0522e刮|U+0522e刮|U+098b3颳|
-U+0540e后|U+05f8c後|U+0540e后|
+U+0540e后|U+0540e后|U+05f8c後|
U+080e1胡|U+080e1胡|U+09b0d鬍|U+0885a衚|
U+056de回|U+056de回|U+08ff4迴|
-U+04f19伙|U+05925夥|U+04f19伙|
-U+059dc姜|U+08591薑|U+059dc姜|
+U+04f19伙|U+04f19伙|U+05925夥|
+U+059dc姜|U+059dc姜|U+08591薑|
U+0501f借|U+0501f借|U+085c9藉|
U+0514b克|U+0514b克|U+0524b剋|
U+056f0困|U+056f0困|U+0774f睏|
@@ -21,15 +21,16 @@ U+06f13漓|U+06f13漓|U+07055灕|
U+091cc里|U+091cc里|U+088e1裡|U+088cf裏|
U+05e18帘|U+07c3e簾|U+05e18帘|
U+09709霉|U+09709霉|U+09ef4黴|
-U+09762面|U+09762面|U+09eb5麵|
+U+09762面|U+09762面|U+09eb5麵|U+09eaa麪|U+09eab麫|
U+08511蔑|U+08511蔑|U+0884a衊|
U+05343千|U+05343千|U+097c6韆|
U+079cb秋|U+079cb秋|U+097a6鞦|
U+0677e松|U+0677e松|U+09b06鬆|
U+054b8咸|U+054b8咸|U+09e79鹹|
U+05411向|U+05411向|U+056ae嚮|U+066cf曏|
-U+04f59余|U+09918餘|U+04f59余|
-U+090c1郁|U+09b31鬱|U+090c1郁|
+U+04f59余|U+04f59余|U+09918餘|
+U+09980馀|U+09918餘|
+U+090c1郁|U+090c1郁|U+09b31鬱|
U+05fa1御|U+05fa1御|U+079a6禦|
U+0613f愿|U+09858願|U+0613f愿|
U+04e91云|U+096f2雲|U+04e91云|
@@ -39,10 +40,10 @@ U+081f4致|U+081f4致|U+07dfb緻|
U+05236制|U+05236制|U+088fd製|
U+06731朱|U+06731朱|U+07843硃|
U+07b51筑|U+07bc9築|U+07b51筑|
-U+051c6准|U+06e96準|U+051c6准|
+U+051c6准|U+051c6准|U+06e96準|
U+05382厂|U+05ee0廠|U+05382厂|
U+05e7f广|U+05ee3廣|U+05e7f广|
-U+08f9f辟|U+095e2闢|U+08f9f辟|
+U+08f9f辟|U+08f9f辟|U+095e2闢|
U+0522b别|U+05225別|U+05f46彆|
U+0535c卜|U+0535c卜|U+08514蔔|
U+06c88沈|U+06c88沈|U+0700b瀋|
@@ -51,75 +52,75 @@ U+079cd种|U+07a2e種|U+079cd种|
U+0866b虫|U+087f2蟲|U+0866b虫|
U+062c5担|U+064d4擔|U+062c5担|
U+0515a党|U+09ee8黨|U+0515a党|
-U+06597斗|U+09b25鬥|U+06597斗|
+U+06597斗|U+06597斗|U+09b25鬥|
U+0513f儿|U+05152兒|U+0513f儿|
-U+05e72干|U+04e7e乾|U+05e79幹|U+05e72干|U+069a6%G榦%@|
+U+05e72干|U+05e72干|U+04e7e乾|U+05e79幹|U+069a6榦|
U+08c37谷|U+08c37谷|U+07a40穀|
U+067dc柜|U+06ac3櫃|U+067dc柜|
U+05408合|U+05408合|U+095a4閤|
-U+05212划|U+05283劃|U+05212划|
+U+05212划|U+05212划|U+05283劃|
U+0574f坏|U+058de壞|U+0574f坏|
U+051e0几|U+05e7e幾|U+051e0几|
U+07cfb系|U+07cfb系|U+07e6b繫|U+04fc2係|
U+05bb6家|U+05bb6家|U+050a2傢|
U+04ef7价|U+050f9價|U+04ef7价|
U+0636e据|U+064da據|U+0636e据|
-U+05377卷|U+06372捲|U+05377卷|
+U+05377卷|U+05377卷|U+06372捲|
U+09002适|U+09069適|U+09002适|
U+08721蜡|U+0881f蠟|U+08721蜡|
U+0814a腊|U+081d8臘|U+0814a腊|
U+04e86了|U+04e86了|U+077ad瞭|
U+07d2f累|U+07d2f累|U+07e8d纍|
-U+04e48么|U+09ebd麽|U+04e48么|U+05e7a幺|U+09ebc麼|
+U+04e48么|U+04e48么|U+09ebd麽|U+05e7a幺|U+09ebc麼|
U+08499蒙|U+08499蒙|U+077c7矇|U+06fdb濛|U+061de懞|
U+04e07万|U+0842c萬|U+04e07万|
U+05b81宁|U+05be7寧|U+05b81宁|
-U+06734朴|U+06a38樸|U+06734朴|
+U+06734朴|U+06734朴|U+06a38樸|
U+082f9苹|U+0860b蘋|U+082f9苹|
-U+04ec6仆|U+050d5僕|U+04ec6仆|
-U+066f2曲|U+066f2曲|U+09eaf麯|
+U+04ec6仆|U+04ec6仆|U+050d5僕|
+U+066f2曲|U+066f2曲|U+09eaf麯|U+09eb4麯|
U+0786e确|U+078ba確|U+0786e确|
U+0820d舍|U+0820d舍|U+06368捨|
U+080dc胜|U+052dd勝|U+080dc胜|
-U+0672f术|U+08853術|U+0672f术|U+0672e朮|
+U+0672f术|U+08853術|U+0672e朮|
U+053f0台|U+053f0台|U+081fa臺|U+06aaf檯|U+098b1颱|
U+04f53体|U+09ad4體|U+04f53体|
U+06d82涂|U+05857塗|U+06d82涂|
U+053f6叶|U+08449葉|U+053f6叶|
U+05401吁|U+05401吁|U+07c72籲|
U+065cb旋|U+065cb旋|U+0955f镟|
-U+04f63佣|U+050ad傭|U+04f63佣|
+U+04f63佣|U+04f63佣|U+050ad傭|
U+04e0e与|U+08207與|U+04e0e与|
U+06298折|U+06298折|U+0647a摺|
-U+05f81征|U+05fb5徵|U+05f81征|
+U+05f81征|U+05f81征|U+05fb5徵|
U+075c7症|U+075c7症|U+07665癥|
U+06076恶|U+060e1惡|U+05641噁|
U+053d1发|U+0767c發|U+09aee髮|
-U+0590d复|U+05fa9復|U+08907複|U+08986覆|
-U+06c47汇|U+0532f匯|U+05f59彙|
+U+0590d复|U+05fa9復|U+08907複|
+U+06c47汇|U+0532f匯|U+06ed9滙|U+05f59彙|
U+083b7获|U+07372獲|U+07a6b穫|
U+09965饥|U+098e2飢|U+09951饑|
U+05c3d尽|U+076e1盡|U+05118儘|
-U+05386历|U+06b77歷|U+066c6曆|
-U+05364卤|U+06ef7滷|U+09e75鹵|
+U+05386历|U+06b77歷|U+066c6曆|U+053a4厤|
+U+05364卤|U+09e75鹵|U+06ef7滷|
U+05f25弥|U+05f4c彌|U+07030瀰|
-U+07b7e签|U+07c3d簽|U+07c56籖|
+U+07b7e签|U+07c3d簽|U+07c64籤|
U+07ea4纤|U+07e96纖|U+07e34縴|
-U+082cf苏|U+08607蘇|U+056cc囌|
+U+082cf苏|U+08607蘇|U+056cc囌|U+07c64甦|
U+0575b坛|U+058c7壇|U+07f48罈|
U+056e2团|U+05718團|U+07cf0糰|
U+0987b须|U+09808須|U+09b1a鬚|
U+0810f脏|U+081df臟|U+09ad2髒|
U+053ea只|U+053ea只|U+096bb隻|
-U+0949f钟|U+09418鐘|U+0937e鍾|
-U+0836f药|U+085e5藥|U+0846f葯|
+U+0949f钟|U+0937e鍾|U+09418鐘|
+U+0836f药|U+0846f葯|U+085e5藥|
U+0540c同|U+0540c同|U+08855衕|
U+05fd7志|U+05fd7志|U+08a8c誌|
U+0676f杯|U+0676f杯|U+076c3盃|
U+05cb3岳|U+05cb3岳|U+05dbd嶽|
U+05e03布|U+05e03布|U+04f48佈|
U+05f53当|U+07576當|U+05679噹|
-U+0540a吊|U+05f14弔|U+0540a吊|
+U+0540a吊|U+0540a吊|U+05f14弔|
U+04ec7仇|U+04ec7仇|U+08b8e讎|
U+08574蕴|U+0860a蘊|U+085f4藴|
U+07ebf线|U+07dda線|U+07dab綫|
@@ -140,10 +141,10 @@ U+060ab悫|U+06128愨|U+06164慤|
U+06781极|U+06975極|U+06781极|
U+06ca9沩|U+06e88溈|U+06f59潙|
U+07618瘘|U+0763a瘺|U+0763b瘻|
-U+07877硷|U+09e7c鹼|U+07906礆|
+U+07877硷|U+07906礆|U+09e7c鹼|
U+07ad6竖|U+08c4e豎|U+07aea竪|
U+07edd绝|U+07d55絕|U+07d76絶|
-U+07ee3绣|U+07e61繡|U+07d89綉|
+U+07ee3绣|U+07d89綉|U+07e61繡|
U+07ee6绦|U+07d5b絛|U+07e27縧|
U+07ef1绱|U+07dd4緔|U+0979d鞝|
U+07ef7绷|U+07db3綳|U+07e43繃|
@@ -158,7 +159,7 @@ U+08d43赃|U+08d13贓|U+08d1c贜|
U+08d4d赍|U+09f4e齎|U+08ceb賫|
U+08d5d赝|U+08d17贗|U+08d0b贋|
U+0915d酝|U+0919e醞|U+09196醖|
-U+091c7采|U+063a1採|U+091c7采|U+057f0埰|
+U+091c7采|U+091c7采|U+063a1採|U+057f0埰|
U+094a9钩|U+09264鉤|U+0920e鈎|
U+094b5钵|U+07f3d缽|U+09262鉢|
U+09508锈|U+092b9銹|U+093fd鏽|
@@ -169,10 +170,35 @@ U+09562镢|U+09481钁|U+0941d鐝|
U+09605阅|U+095b1閱|U+095b2閲|
U+09893颓|U+09839頹|U+0983d頽|
U+0989c颜|U+0984f顏|U+09854顔|
-U+09980馀|U+09918餘|
U+09a82骂|U+07f75罵|U+099e1駡|
U+09c87鲇|U+09bf0鯰|U+09b8e鮎|
U+09c9e鲞|U+09bd7鯗|U+09b9d鮝|
U+09cc4鳄|U+09c77鱷|U+09c10鰐|
U+09e21鸡|U+096de雞|U+09dc4鷄|
U+09e5a鹚|U+09dbf鶿|U+09dc0鷀|
+U+054c4哄|U+054c4哄|U+09b28鬨|
+U+05582喂|U+05582喂|U+09935餵|
+U+06e38游|U+06e38游|U+0904a遊|
+U+04e8e于|U+04e8e于|U+065bc於|
+U+05446呆|U+05446呆|U+07343獃|
+U+096c7雇|U+096c7雇|U+050f1僱|
+U+05978奸|U+05978奸|U+059e6姦|
+U+068f1棱|U+068f1棱|U+07a1c稜|
+U+05347升|U+05347升|U+06607昇|U+0965e陞|
+U+06258托|U+06258托|U+08a17託|
+U+0633d挽|U+0633d挽|U+08f13輓|
+U+05e78幸|U+05e78幸|U+05016倖|
+U+06d8c涌|U+06d8c涌|U+06e67湧|
+U+06b32欲|U+06b32欲|U+0617e慾|
+U+0624e扎|U+0624e扎|U+07d2e紮|
+U+05360占|U+05360占|U+04f54佔|
+U+06ce8注|U+06ce8注|U+08a3b註|
+U+04ed1仑|U+04f96侖|U+05d19崙|
+U+06817栗|U+06817栗|U+06144慄|
+U+05742坂|U+05742坂|U+0962a阪|
+U+096d5雕|U+096d5雕|U+09d70鵰|
+U+05398厘|U+05398厘|U+091d0釐|
+U+06881梁|U+06881梁|U+06a11樑|
+U+05e84庄|U+05e84庄|U+0838a莊|
+U+062fc拼|U+062fc拼|U+062da拚|
+U+08d5e赞|U+08d0a贊|U+08b9a讚|
diff --git a/includes/zhtable/simp2trad_noconvert.manual b/includes/zhtable/simp2trad_noconvert.manual
new file mode 100644
index 00000000..5ad656b3
--- /dev/null
+++ b/includes/zhtable/simp2trad_noconvert.manual
@@ -0,0 +1,4 @@
+著
+竈
+彞
+余
diff --git a/includes/zhtable/simp2trad_supp_set.manual b/includes/zhtable/simp2trad_supp_set.manual
new file mode 100644
index 00000000..0f479906
--- /dev/null
+++ b/includes/zhtable/simp2trad_supp_set.manual
@@ -0,0 +1,2 @@
+余 餘
+着 著
diff --git a/includes/zhtable/simpphrases.manual b/includes/zhtable/simpphrases.manual
new file mode 100644
index 00000000..8e754b7f
--- /dev/null
+++ b/includes/zhtable/simpphrases.manual
@@ -0,0 +1,389 @@
+乾坤
+乾隆
+旋乾转坤
+乾陵
+乾县
+康乾
+乾嘉
+乾盛世
+郭子乾
+张法乾
+萧乾
+乾旦
+乾断
+乾图
+乾纲
+乾红
+乾乾
+乾清宫
+乾象;
+乾宅;
+乾造;
+乾曜;
+乾元;
+乾卦;
+李乾德;
+挨着
+爱着
+暗着
+昂着
+摆着
+伴着
+办着
+帮着
+绑着
+抱着
+背着
+备着
+本着
+本著作
+本著名
+本著者
+逼着
+闭着
+变着
+不着边际
+不着痕迹
+猜着
+踩着
+藏着
+侧着
+缠着
+敞着
+唱着
+朝着
+沉着
+乘着
+持着
+斥着
+丑着
+穿着
+吹着
+达着
+打着
+待着
+带着
+戴着
+当着
+挡着
+得着
+得著作
+得著者
+得著名
+瞪着
+低着
+点着
+盯着
+顶着
+定着
+定著作
+动着
+斗着
+独着
+对着
+对著名
+对著作
+对著者
+盾着
+犯不着
+福着
+赶着
+高着
+隔着
+跟着
+孤着
+关着
+关著作
+关著名
+关著者
+管着
+惯着
+光着
+光著作
+跪着
+裹着
+撼着
+喝着
+候着
+怀着
+晃着
+挥着
+活着
+获着
+获着
+急着
+记着
+冀着
+夹着
+驾着
+见着
+闲着
+叫着
+接着
+借着
+借着
+据着
+据著作
+据著名
+据著者
+开着
+看着
+康着
+扛着
+考着
+渴着
+刻着
+空着
+哭着
+苦着
+捆着
+困着
+拉着
+来着
+乐着
+乐著作
+努力着
+丽着
+连着
+连著作
+连著名
+连著者
+恋着
+凉着
+亮着
+临着
+拎着
+领着
+流着
+留着
+搂着
+陋着
+落着
+骂着
+瞒着
+漫着
+忙着
+冒着
+美着
+美著作
+美著名
+美著者
+梦着
+蒙着
+拿着
+逆着
+酿着
+努着
+趴着
+跑着
+陪着
+配着
+披着
+骗着
+飘着
+拼着
+铺着
+骑着
+牵着
+求着
+去着
+嚷着
+绕着
+忍着
+揉着
+润着
+烧着
+身着
+沉着
+盛着
+试着
+守着
+受着
+受著作
+受著名
+受著者
+梳着
+竖着
+数着
+睡不着
+睡着
+顺着
+随着
+踏着
+抬着
+躺着
+提着
+甜着
+挑着
+跳着
+听着
+听著名
+听著作
+听著者
+听着
+听著名
+听著作
+听著者
+偷着
+拖着
+望着
+围着
+味着
+想着
+响着
+向着
+笑着
+心着
+新著龙虎门
+信着
+行着
+性着
+性著作
+性著名
+性著者
+性著述
+学着
+学著作
+寻着
+循着
+压着
+雅着
+沿着
+耀着
+掖着
+衣着
+疑着
+溢着
+艺着
+因着
+印着
+应着
+映着
+用不着
+用着
+用著作
+悠着
+有着
+有著名
+有著作
+有著者
+与着
+与著名
+与著作
+与著者
+语着
+豫着
+远着
+跃着
+杂着
+载着
+在着
+在著作
+在著名
+在著者
+扎着
+展着
+站着
+战着
+蘸着
+仗着
+找不着
+照着
+照著名
+照著作
+照著者
+罩着
+贞着
+枕着
+争着
+挣着
+制着
+志着
+皱着
+住着
+着笔
+着鞭
+着法
+着火
+着急
+着舰
+着脚
+着她
+着紧
+着力
+着凉
+着陆
+着录
+着落
+着忙
+着迷
+着墨
+着妳
+着你
+着色
+着什么急
+着实
+着手
+着数
+着丝
+着他
+着它
+着祂
+着我
+着想
+着眼
+着衣
+着意
+着重
+着重
+着装
+抓着
+转着
+装着
+追着
+髭着
+走着
+坐着
+做着
+含着
+含著名
+含著作
+含著者
+涵着
+涵著名
+涵著作
+涵著者
+演着
+演著名
+演著作
+演著者
+保障着
+保障著名
+保障著作
+保障著者
+黏着
+胶着
+附着
+代表着
+代表著名
+代表著作
+代表著者
+着地
+浮着
+写着
+写著作
+写著名
+遇着
+於乎
+於戏
+魏徵
+柳诒徵
+於姓
+於氏
+於夫罗
+於梨华
+卷舌
+樊於期
+於菟
+於潜县
+馀年
diff --git a/includes/zhtable/simpphrases_exclude.manual b/includes/zhtable/simpphrases_exclude.manual
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/includes/zhtable/simpphrases_exclude.manual
diff --git a/includes/zhtable/toCN.manual b/includes/zhtable/toCN.manual
index 427afad2..bc2222f4 100644
--- a/includes/zhtable/toCN.manual
+++ b/includes/zhtable/toCN.manual
@@ -1,3 +1,7 @@
+」 ”
+「 “
+『 ‘
+』 ’
記憶體 内存
預設 默认
串列 串行
@@ -63,7 +67,6 @@
資料庫 数据库
乳酪 奶酪
鉅賈 巨商
-手電筒 手电
萬曆 万历
永曆 永历
辭彙 词汇
@@ -262,12 +265,11 @@
冷盤   凉菜
冷菜 凉菜
散钱 零钱
-谐星 笑星    
+谐星 笑星
夜学 夜校
华乐 民乐
中樂 民乐
屋价 房价
-的士 出租车
計程車 出租车
公車 公共汽车
單車 自行车
@@ -283,6 +285,8 @@
衛生 卫生
賓士 奔驰
平治 奔驰
+平治之亂 平治之乱
+平治之乱 平治之乱
積架 捷豹
福斯 大众
福士 大众
@@ -305,4 +309,3 @@
舒麥加 迈克尔·舒马赫
希特拉 希特勒
黛安娜 戴安娜
-希拉 赫拉
diff --git a/includes/zhtable/toHK.manual b/includes/zhtable/toHK.manual
index 6a872fa6..9afddb77 100644
--- a/includes/zhtable/toHK.manual
+++ b/includes/zhtable/toHK.manual
@@ -1,3 +1,11 @@
+” 」
+“ 「
+‘ 『
+’ 』
+凶殺 兇殺
+凶殘 兇殘
+緝凶 緝兇
+買凶 買兇
打印机 打印機
印表機 打印機
字节 位元組
@@ -168,6 +176,8 @@
速食麵 即食麵
泡麵 即食麵
土豆 馬鈴薯
+土豆网 土豆網
+土豆網 土豆網
华乐 中樂
民乐 中樂
計程車 的士
diff --git a/includes/zhtable/toSG.manual b/includes/zhtable/toSG.manual
index 9a399bc8..5f7cb0ca 100644
--- a/includes/zhtable/toSG.manual
+++ b/includes/zhtable/toSG.manual
@@ -1,3 +1,7 @@
+」 ”
+「 “
+『 ‘
+』 ’
方便面 快速面
速食麵 快速面
即食麵 快速面
@@ -12,4 +16,4 @@
民乐 华乐
住房 住屋
房价 屋价
-泡麵 快速面 \ No newline at end of file
+泡麵 快速面
diff --git a/includes/zhtable/toSimp.manual b/includes/zhtable/toSimp.manual
new file mode 100644
index 00000000..d18fc8c7
--- /dev/null
+++ b/includes/zhtable/toSimp.manual
@@ -0,0 +1,21 @@
+乾县 乾县
+萧乾 萧乾
+乾断 乾断
+乾图 乾图
+乾纲 乾纲
+乾红 乾红
+乾清宫 乾清宫
+柳诒徵 柳诒徵
+於夫罗 於夫罗
+於梨华 於梨华
+于潜县 於潜县
+憑藉 凭借
+藉端 借端
+藉故 借故
+藉口 借口
+藉助 借助
+藉手 借手
+藉詞 借词
+藉機 借机
+藉此 借此
+藉由 借由
diff --git a/includes/zhtable/toTW.manual b/includes/zhtable/toTW.manual
index a1639f7f..13a81a69 100644
--- a/includes/zhtable/toTW.manual
+++ b/includes/zhtable/toTW.manual
@@ -1,3 +1,24 @@
+” 」
+“ 「
+‘ 『
+’ 』
+着 著
+元凶 元凶
+凶器 凶器
+凶徒 凶徒
+凶手 凶手
+凶案 凶案
+凶残 凶殘
+凶杀 凶殺
+疑凶 疑凶
+真凶 真凶
+缉凶 緝凶
+行凶 行凶
+行凶后 行凶後
+买凶 買凶
+追凶 追凶
+复苏 復甦
+復蘇 復甦
缺省 預設
串行 串列
以太网 乙太網
@@ -15,10 +36,12 @@
脱机 離線
声卡 音效卡
老字号 老字號
+连字号 連字號
字号 字型大小
字库 字型檔
字段 欄位
字符 字元
+字符集 字符集
存盘 存檔
寻址 定址
尾注 章節附註
@@ -35,6 +58,9 @@
磁盘 磁碟
磁道 磁軌
程控 程式控制
+远程控制 遠程控制
+遠程控制 遠程控制
+行程控制 行程控制
端口 埠
算子 運算元
算法 演算法
@@ -66,11 +92,13 @@
奶酪 乳酪
巨商 鉅賈
手电 手電筒
+手电筒 手電筒
万历 萬曆
永历 永曆
词汇 辭彙
习用 慣用
元音 母音
+宋元 宋元
任意球 自由球
头球 頭槌
入球 進球
@@ -95,6 +123,7 @@
網絡 網路
人工智能 人工智慧
航天飞机 太空梭
+航天大学 航天大學
穿梭機 太空梭
因特网 網際網路
互聯網 網際網路
@@ -259,6 +288,8 @@
快速面 速食麵
即食麵 速食麵
薯仔 土豆
+土豆网 土豆網
+土豆網 土豆網
蹦极跳 笨豬跳
绑紧跳 笨豬跳
冷菜 冷盤
@@ -269,6 +300,8 @@
雪糕 冰淇淋
卫生 衛生
衞生 衛生
+平治之亂 平治之亂
+平治之乱 平治之亂
平治 賓士
奔驰 賓士
積架 捷豹
@@ -287,4 +320,3 @@
凡高 梵谷
狄安娜 黛安娜
戴安娜 黛安娜
-赫拉 希拉
diff --git a/includes/zhtable/toTrad.manual b/includes/zhtable/toTrad.manual
new file mode 100644
index 00000000..76d0ab58
--- /dev/null
+++ b/includes/zhtable/toTrad.manual
@@ -0,0 +1,42 @@
+手塚治虫 手塚治虫
+校仇 校讎
+仇校 讎校
+仇夷 讎夷
+仇問 讎問
+無言不仇 無言不讎
+視如寇仇 視如寇讎
+往日無仇 往日無讎
+近日無仇 近日無讎
+李連杰 李連杰
+周杰倫 周杰倫
+寶曆 寶曆
+涂謹申 涂謹申
+於姓 於姓
+於氏 於氏
+於夫羅 於夫羅
+於梨華 於梨華
+鄭凱云 鄭凱云
+筑陽 筑陽
+筑後 筑後
+采石磯 采石磯
+采石之戰 采石之戰
+張三丰 張三丰
+丰韻 丰韻
+丰儀 丰儀
+丰標不凡 丰標不凡
+干細胞 幹細胞
+干熱 乾熱
+二里頭 二里頭
+水里鄉 水里鄉
+蒙胧 朦朧
+酒曲 酒麴
+呆里呆气 呆裡呆氣
+拜托 拜託
+委托书 委託書
+委托 委託
+挽詞 輓詞
+挽聯 輓聯
+挽詩 輓詩
+於夫罗 於夫羅
+府干預 府干預
+府干擾 府干擾
diff --git a/includes/zhtable/trad2simp.manual b/includes/zhtable/trad2simp.manual
index da069310..458a3c92 100644
--- a/includes/zhtable/trad2simp.manual
+++ b/includes/zhtable/trad2simp.manual
@@ -13,3 +13,14 @@ U+056ae嚮|U+05411向|
U+09031週|U+05468周|
U+0904a遊|U+06e38游|
U+06de9淩|U+051cc凌|
+U+095e2闢|U+08f9f辟|
+U+05bc0寀|U+091c7采|
+U+08518蔘|U+053c2参|
+U+06b05欅|U+06989榉|
+U+07343獃|U+05446呆|
+U+09918餘|U+04f59余|U+09980馀|
+U+09592閒|U+095f2闲|
+U+08b6d譭|U+06bc1毁|
+U+071ec燬|U+06bc1毁|
+U+08457著|U+08457著|U+07740着|
+U+05d11崑|U+06606昆|
diff --git a/includes/zhtable/trad2simp_noconvert.manual b/includes/zhtable/trad2simp_noconvert.manual
new file mode 100644
index 00000000..6efab3d4
--- /dev/null
+++ b/includes/zhtable/trad2simp_noconvert.manual
@@ -0,0 +1,4 @@
+"余"=>
+碁
+藉
+=>"獃"
diff --git a/includes/zhtable/trad2simp_supp_set.manual b/includes/zhtable/trad2simp_supp_set.manual
new file mode 100644
index 00000000..6e6ed8ca
--- /dev/null
+++ b/includes/zhtable/trad2simp_supp_set.manual
@@ -0,0 +1 @@
+著 着
diff --git a/includes/zhtable/trad2simp_supp_unset.manual b/includes/zhtable/trad2simp_supp_unset.manual
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/includes/zhtable/trad2simp_supp_unset.manual
diff --git a/includes/zhtable/tradphrases.manual b/includes/zhtable/tradphrases.manual
index b2fec815..9caa3cc5 100644
--- a/includes/zhtable/tradphrases.manual
+++ b/includes/zhtable/tradphrases.manual
@@ -1,4 +1,7 @@
+零隻
一隻
+二隻
+兩隻
三隻
四隻
五隻
@@ -11,10 +14,188 @@
千隻
萬隻
億隻
+多隻
+0多隻
+零多隻
+十多隻
+百多隻
+千多隻
+萬多隻
+億多隻
+數天後
+幾天後
+多天後
+零天後
+一天後
+二天後
+兩天後
+三天後
+四天後
+五天後
+六天後
+七天後
+八天後
+九天後
+十天後
+百天後
+千天後
+萬天後
+億天後
+0天後
+1天後
+2天後
+3天後
+4天後
+5天後
+6天後
+7天後
+8天後
+9天後
+0天後
+1天後
+2天後
+3天後
+4天後
+5天後
+6天後
+7天後
+8天後
+9天後
+後印
+萬象
並存著
乾絲
乾著急
-体育鍛鍊
+乾魚
+魚乾
+乾梅
+糕乾
+黃乾黑廋
+馬乾
+香乾
+趲幹
+謀幹
+詞幹
+蟶乾
+薄幹
+腦幹
+營幹
+老乾
+老幹部
+管幹
+盲幹
+煨乾
+海乾
+乾漆
+淚乾
+沒幹
+沒乾沒淨
+枝不得大於榦
+杯乾
+朝乾夕惕
+打幹
+打乾噦
+徐幹
+府幹
+乾館
+乾顙
+幹革命
+乾霍亂
+乾雷
+乾阿奶
+乾量
+乾醋
+乾逼
+乾貨
+乾衣
+幹蠱
+乾虔
+乾落
+幹營生
+乾茶錢
+乾茨臘
+乾苔
+乾花
+乾肥
+乾耗
+幹缺
+乾繃
+乾結
+乾餱
+乾篾片
+乾稿
+乾禮
+乾瞪眼
+乾白兒
+乾疥
+乾生子
+乾生受
+幹父之蠱
+乾熬
+乾燈盞
+乾濕
+乾澀
+幹濟
+乾沒
+乾死
+乾村沙
+乾暖
+乾料
+乾敲梆子不賣油
+乾支支
+乾支剌
+乾擦
+乾撇下
+乾撂台
+乾折
+乾急
+幹當
+乾式
+乾屎橛
+幹家
+乾奴才
+幹頭
+乾塢
+乾圓潔淨
+乾回付
+乾啼
+乾哭
+乾噦
+乾咽
+乾和
+幹吏
+乾吊著下巴
+乾號
+乾颱
+乾卦
+乾剝剝
+乾刻版
+乾芻
+幹人
+乾產
+乾喬
+夯幹
+大目乾連
+國之楨榦
+唇乾
+單幹
+勾幹
+不幹性油
+不幹不淨
+豆乾
+果乾
+如果幹
+乾麵
+乾柴
+枯乾
+晒乾
+顛乾倒坤
+強幹
+乾著
+乾眼
+幹的停當
+乾巴
+偎乾
借著
偷雞不著
几絲
@@ -103,7 +284,6 @@
罵著
肉絲麵
背向著
-菌絲体
菌絲體
著兒
著書立說
@@ -119,6 +299,7 @@
被覆著
覆著
覆蓋著
+反覆
訴說著
說著
請參閱
@@ -134,7 +315,6 @@
醞釀著
錄著
鍛鍊出
-鍛鍊身体
關係著
雞絲
雞絲麵
@@ -143,7 +323,1343 @@
顯著標志
颳著
髮絲
+斷髮
+披頭散髮
+髮禁
鬥著
鬧著玩儿
鬧著玩兒
鯰魚
+世界盃
+其次辟地
+開闢
+闢地
+精闢
+別闢
+另闢
+闢佛
+闢田
+闢築
+闢謠
+闢辟
+透闢
+墾闢
+翕闢
+軒闢
+闢建
+闢室
+各闢
+增闢
+闢邪以律
+錶盤
+錶板
+錶帶
+錶針
+錶蒙子
+彆口氣
+彆強
+皺彆
+一彆頭
+并州
+併兼
+併產
+併骨
+併網
+併線
+併流
+逼併
+併名
+併當
+併火
+併肩子
+併兼
+併除
+併疊
+忙併
+打併
+並發表
+並發現
+並發展
+並發動
+舉手表
+揮手表
+併一不二
+連三併四
+相併
+撤併
+數罪併罰
+催併
+狂併潮
+薝蔔
+提摩太後書
+當家纔知柴米價
+剛纔一載
+裏海
+骨頭裡掙出來的錢纔做得肉
+恰纔
+遠縣纔至
+別日南鴻纔北去
+然身死纔數月耳
+纔得兩年
+纔則
+纔此
+你纔子發昏
+纔可容顏十五餘
+不採
+披榛採蘭
+謬採虛聲
+採樵人
+回採
+觀採
+開採
+揪採
+樵採
+採訪
+採辦
+採補
+採買
+採風問俗
+採納
+採獵
+採蓮
+採錄
+採購
+採光
+採礦
+採花
+採集
+採擷
+採掘
+採芹人
+採取
+採選
+採摭
+採摘
+採珠
+採種
+採茶
+採石
+採拾
+採收
+採生折割
+採樹種
+採擇
+採藥
+採薇
+採用
+盜採
+採信
+採行
+採證
+採菊
+博採
+採空採穗
+採挖
+採鐵
+採金
+採氣
+採油
+採煤
+採鹽
+採區
+採運
+採風
+官地為寀
+寮寀
+蔘綏
+人蔘
+蕭蔘
+人參與
+人參選
+人參觀
+人參考
+人參展
+人參加
+人參議
+人參謀
+人參酌
+人參照
+人參政
+人參戰
+人參拜
+人參閱
+人參禪
+人參贊
+人參見
+人參透
+人參看
+東衝西突
+天克地衝
+六衝
+撞陣衝軍
+衝波
+衝風
+衝頭陣
+衝堅陷陣
+衝陷
+衝心
+衝州撞府
+衝殺
+衝然
+衝盹
+左衝右突
+虫部
+手塚治虫
+群醜
+百拙千醜
+大醜
+地醜德齊
+丟醜
+亮醜
+揭醜
+倛醜
+嫌好道醜
+醜巴怪
+醜末
+醜婦
+醜地
+醜頭怪臉
+醜女效顰
+醜剌剌
+醜話
+醜媳
+醜吒
+醜生
+醜聲遠播
+醜夷
+弄醜
+露醜
+摧堅獲醜
+謷醜
+不嫌母醜
+一爭兩醜
+惡直醜正
+么麼小丑
+齣電影
+齣卡通
+齣戲
+齣劇
+平平當當
+滿滿當當
+當當丁丁
+丁丁當當
+停停當當
+快快當當
+咯噹
+啷噹
+党參
+党進
+党太尉
+党項
+撲鼕
+洗髮
+牽一髮
+白發其事
+后髮座
+波髮藻
+辮髮
+逋髮
+抿髮
+髮漂
+髮匪
+髮光可鑒
+髮腳
+髮癬
+髮釵
+髮飾
+髮紗
+髮上指冠
+髮上沖冠
+髮乳
+髮引千鈞
+髮踴沖冠
+董氏封髮
+胎髮
+禿妃之髮
+捉髮
+綠髮
+括髮
+髡髮
+鵠髮
+截髮
+解髮佯狂
+淨髮
+秋髮
+噙齒戴髮
+青山一髮
+晞髮
+細不容髮
+心細如髮
+祝髮
+擢髮
+齒髮
+齒危髮秀
+沖冠髮怒
+甩髮
+絲髮
+絲恩髮怨
+蒜髮
+算髮
+有髮頭陀寺
+髮箋
+髮屋
+櫛髮工
+鬒髮
+模范棒棒堂
+顏範
+儀範
+典範
+坤範
+壼範
+容範
+懿範
+明範
+格範
+模範
+樣範
+母範
+洪範
+淑範
+遺範
+科範
+立範
+貽範
+道範
+閨範
+閫範
+雅範
+霽範
+顏範
+鴻範
+道範
+壼範
+霽範
+容範
+懿範
+樣範
+沒樣範
+丰采
+丰標不凡
+丰神
+丰茸
+丰儀
+丰度
+丰情
+丰韵
+子之丰兮
+艸木丰丰
+張三丰
+復始
+複分析
+複輔音
+複元音
+複平面
+複流
+反複製
+顛覆
+答覆
+覆沒
+覆亡
+覆水難收
+翻雲覆雨
+覆雨翻雲
+覆轍
+覆巢之下無完卵
+覆蓋
+覆命
+天翻地覆
+天覆地載
+撥穀
+扁擬穀盜蟲
+不穀
+辟穀
+米穀
+田穀
+脫穀機
+年穀
+礱穀機
+孤寡不穀
+穀米
+穀旦
+穀圭
+穀貴餓農
+穀食
+穀日
+館穀
+禾穀
+積穀
+嘉穀
+嚼穀
+九穀
+戩穀
+錢穀
+息穀
+殖穀
+川穀
+曬穀
+臧穀亡羊
+種穀
+颳雪
+刮風下雪倒便宜
+广部
+亂鬨不過來
+斗鬨
+亂鬨
+開鬨
+花鬨
+鬨動
+交鬨
+喧鬨
+起鬨
+內鬨
+於後
+猜三划五
+划龍舟
+南迴線
+南迴鐵路
+北迴線
+北迴鐵路
+文匯報
+河流匯集
+品彙
+博彙
+滙豐
+伙頭
+方几
+伏几
+高几
+雪窗螢几
+燕几
+隱几
+饑饉
+乾薑
+毛薑
+薑母
+薑湯
+薑桂
+薑是老的辣
+吃薑
+薑老辣
+野薑
+咬薑呷醋
+薑蓉
+薑黃
+駘藉
+躪藉
+凌藉
+顧藉
+狐藉虎威
+滑藉
+藉槁
+藉寇兵
+藉卉
+藉箸代籌
+藉手
+藉甚
+藉草枕塊
+枕經藉史
+死傷相藉
+素藉
+茵藉
+龍捲
+捲舌
+夸父
+夸克
+夸特
+夸毗
+夸麗
+夸姣
+夸人
+夸容
+大言非夸
+言大而夸
+睏覺
+愛睏
+纍堆
+纍紲
+纍臣
+纍瓦結繩
+湘纍
+印纍綬若
+灕湘
+灕然
+澤滲灕而下降
+裏勾外連
+裏手
+水里鄉
+二里頭
+年歷史
+西歷史
+國歷史
+國歷代
+新歷史
+夏歷史
+百花曆
+寶曆
+穆罕默德曆
+大明曆
+大曆
+台曆
+太初曆
+通曆
+曆本
+曆命
+曆紀
+曆始
+曆室
+曆日
+曆尾
+曆元
+律曆志
+官曆
+回曆
+巧曆
+慶曆
+朱理安曆
+長曆
+藏曆
+四分曆
+三統曆
+額我略曆
+埃及曆
+伊斯蘭教曆
+合曆
+玉曆
+農民曆
+桌曆
+商曆
+周曆
+大衍曆
+皇極曆
+儒略改革曆
+希伯來曆
+厤物之意
+爰定祥厤
+白黴
+黴黧
+黴黑
+麴黴
+蒙霧露
+懞懞懂懂
+懞直
+老懞
+放懞掙
+矇著
+矇聵
+矇瞍
+矇事
+矇頭轉
+矇松雨
+藏矇歌兒
+矇著鍋兒
+朦朧
+濛濛細雨
+濛汜
+冥濛
+溟濛
+淡濛濛
+凌濛初
+涳濛
+灰濛濛
+澒濛
+瀰山遍野
+瀰瀰
+冷麵
+撈麵
+煮麵
+炆麵
+煎麵
+泡麵
+食麵
+公仔麵
+方便麵
+白粉麵
+棒子麵
+麵缸
+麵坯兒
+麵碼兒
+麵坊
+麵湯
+麵疙瘩
+麵館
+麵漿
+甜水麵
+麵人兒
+麵塑
+捏麵人
+趕麵棍
+擀麵
+過水麵
+蕎麥麵
+巧婦做不得無麵餺飥
+削麵
+小米麵
+壯麵
+吃板刀麵
+吃辣麵
+扯麵
+搋麵
+重羅麵
+雜麵
+雜合麵兒
+溲麵
+索麵
+一鍋麵
+伊府麵
+藥麵兒
+洋麵
+意大利麵
+湯下麵
+茶麵
+冷面相
+糞穢衊面
+湟潦生苹
+食野之苹
+苹縈
+青苹
+僕僕
+有僕
+冉有僕
+屢顧爾僕
+僕少
+僕雖罷駑
+僕夫
+僕僮
+僕吏
+僕姑
+僕固懷恩
+僕程
+僕使
+僕憎
+僕歐
+僕射
+太僕
+僮僕
+金僕姑
+樸實
+樸訥
+樸念仁
+白樸
+抱素懷樸
+抱朴而長吟兮
+樸鄙
+樸馬
+樸父
+樸訥
+樸陋
+樸魯
+樸厚
+樸學
+樸質
+樸拙
+樸重
+樸實
+樸素
+樸樕
+樸野
+反樸
+古樸
+胡樸安
+返樸
+渾樸
+儉樸
+簡樸
+拙樸
+斫雕為樸
+斲雕為樸
+斲雕為樸
+質樸
+誠樸
+純樸
+曾樸
+郁樸
+棫樸
+敦樸
+樸鈍
+樸直
+見素抱樸
+掣籤
+標籤
+書籤
+發籤
+粉籤子
+路籤
+更籤
+好籤
+火籤
+籤幐
+籤押
+照入籤
+制籤
+抽公籤
+瑤籤
+藥籤
+萬籤插架
+雲笈七籤
+犖确
+磽确
+确瘠
+言辯而确
+數與虜确
+關弓與我确
+拚捨
+廣捨
+齊王捨牛
+捨墮
+捨實
+棄捨
+捨安就危
+施舍之道
+瀋河
+瀋水
+瀋州
+瀋山線
+瀋吉線
+墨沈
+瀋海鐵路
+遼瀋
+胜肽
+胜鍵
+雙胜類
+兀朮
+白朮
+蒼朮
+朮赤
+髼鬆
+皮鬆
+濛鬆雨
+發鬆
+翻鬆
+浮鬆
+弄鬆
+精鬆
+懈鬆
+鬆蛋
+鬆寬
+鬆氣
+鬆一口氣
+囉囉囌囌
+囉囌
+骨罈
+罈騞
+餵驢
+剪牡丹喂牛
+鹹粥
+鹹食
+鹹潟
+鹹嘴淡舌
+鹽打怎麼鹹
+鹹派
+鹹批
+錦綉花園
+籲天
+勃鬱
+怫鬱
+氣鬱
+沉鬱
+神荼鬱壘
+躁鬱
+蒼鬱
+漚鬱
+伊鬱
+壹鬱
+悒鬱
+氤鬱
+湮鬱
+陰鬱
+泱鬱
+坱鬱
+滃鬱
+蓊鬱
+紆鬱
+鬱勃
+鬱陶
+鬱律
+鬱壘
+鬱火
+鬱積
+鬱金
+鬱江
+鬱血
+鬱蒸
+鬱症
+鬱沉沉
+鬱熱
+鬱塞
+鬱伊
+鬱邑
+鬱挹
+鬱堙不偶
+鬱泱
+鬱蓊
+鬱紆
+鬱燠
+肝鬱
+鬱卒
+鬱鬱不平
+鬱鬱不樂
+鬱鬱寡歡
+鬱鬱蔥蔥
+鬱鬱而終
+愿樸
+愿而恭
+許愿起經
+北嶽
+嶽麓山
+但云
+胡云
+詩云
+注云
+鄭凱云
+云乎
+云然
+云為
+對摺
+網誌
+標標致致
+澄澹精致
+呆緻緻
+光緻緻
+工緻
+功緻
+縝緻
+堅緻
+种放
+种師道
+种師中
+後庄
+舊庄
+正官庄
+龜山庄
+寶山庄
+冬山庄
+員山庄
+松山庄
+厂部
+閤府
+分佈
+剪綵
+衝量
+韶山衝
+衝車
+書獃子
+相干
+府干預
+府干涉
+府干政
+府干擾
+府干犯
+府干卿
+一干人
+未乾
+抹乾
+餅乾
+拭乾
+擦乾
+晾乾
+枯乾
+烘乾
+肉乾
+菜乾
+腐乾
+乾脆
+乾淨
+乾燥
+乾旱
+乾涸
+乾洗
+乾女
+乾等
+乾糧
+乾枯
+乾薪
+乾爹
+乾粉
+乾爽
+乾兒
+乾子
+乾渴
+乾股
+乾果
+乾草
+乾菜
+乾笑
+乾餾
+乾電
+乾飯
+乾冰
+乾嘔
+乾材
+乾媽
+乾季
+葡萄乾
+提子乾
+蘿蔔乾
+蘋果乾
+芒果乾
+菠蘿乾
+鳳梨乾
+豆腐乾
+果子乾
+龍眼乾
+乾乾淨淨
+乾柴烈火
+乾乾兒的
+桑乾
+撈乾
+搭乾鋪
+揩乾
+敢幹
+幹探
+幹事
+幹什麼
+幹細胞
+悶著頭兒幹
+配水幹管
+繐幃飄井幹
+站乾岸兒
+秋陰入井幹
+沒梢幹
+楨幹
+據榦而窺井底
+井榦摧敗
+杰特
+李連杰
+周杰倫
+稜鏡
+稜角
+稜台
+稜錐
+觚稜
+稜子
+稜層
+稜柱
+盧稜伽
+波稜菜
+菠稜菜
+稜縫
+稜等登
+稜稜
+嶒稜
+蹭稜子
+稜體
+二不稜登
+有稜有角
+威稜
+負債纍纍
+傷痕纍纍
+儒略曆
+伊斯蘭曆
+寶曆
+酒麴
+昇平
+爾冬陞
+澹臺
+涂謹申
+拜託
+委託
+輓曲
+敬輓
+万俟
+万旗
+鬚鯨
+鬚鯊
+兇手
+兇徒
+兇案
+兇器
+兇殺
+兇殘
+行兇
+緝兇
+追兇
+真兇
+疑兇
+買兇
+元兇
+叶韻
+叶音
+叶恭弘
+叶 恭弘
+叶 恭弘
+於夫羅
+於梨華
+置於
+佈於
+散於
+播於
+國於
+敗於
+於一役
+畢於
+畢業於
+寒於
+任於
+拘於
+插於
+中於
+於市
+於野
+敏於
+聽於
+短於
+成於
+樊於期
+淡於
+於陸
+於密
+於盡
+禍於
+格於
+猛於
+施於
+於牆
+於物
+於己
+於你
+於我
+於他
+於她
+於它
+於祂
+拒人於
+拒於
+潰於
+窮於
+相於
+形於
+半於
+於始
+於終
+詢於
+美於
+醜於
+好於
+坏於
+強於
+弱於
+差於
+劣於
+於美
+於醜
+於好
+於坏
+於強
+於弱
+於差
+於劣
+於垂
+染指於
+於火
+存十一於千百
+於勤
+隱於
+藏於
+嚴於
+寬於
+於幕
+給於
+於穆
+於呼哀哉
+於時
+於該
+危於
+於伏
+於何
+於家
+於國
+於潛縣
+於焉
+於征
+離於
+於畢
+麗於
+下於
+亞於
+同於
+屑於
+絕於
+致於
+於行
+遜於
+任教於
+教於
+自於
+來於
+附於
+於人
+於世
+阻於
+於民
+於盲
+於色
+囿於
+直於
+建於
+都於
+於農
+於樂
+於前
+役於
+於心
+於法
+於事
+助於
+害於
+損於
+益於
+從於
+隨於
+順於
+汲於
+溺於
+迷於
+醉於
+行於
+泥於
+身於
+足於
+溢於
+於衷
+畏於
+視於
+衷於
+狃於
+疲於
+通於
+於途
+老於
+耿於
+於懷
+服於
+致於
+臻於
+匿於
+因於
+似於
+遷於
+怒於
+心於
+集於
+容於
+髒詞
+髒心
+新紮
+紙紮
+紮鐵
+紮寨
+一紮
+兩紮
+三紮
+四紮
+五紮
+六紮
+七紮
+八紮
+九紮
+十紮
+百紮
+千紮
+萬紮
+佔超過
+獨佔鰲頭
+誌異
+筑前
+筑後
+筑紫
+筑波
+筑州
+筑肥
+筑西
+筑北
+肥筑方言
+筑邦
+筑陽
+南筑
+批准的
+核准的
+為準
+擺鐘
+編鐘
+碰鐘
+鳴鐘
+晨鐘
+飯後鐘
+盜鐘
+當一天和尚撞一天鐘
+殿鐘自鳴
+天文鐘
+洛鐘東應
+亮鐘
+郘鐘
+歌鐘
+鐘不撞不鳴
+毀鐘為鐸
+洪鐘
+擊鐘
+警世鐘
+竊鐘掩耳
+潛水鐘
+琴鐘
+見鐘不打
+釁鐘
+朝鐘
+撞木鐘
+鐘不扣不鳴
+鐘鳴
+鐘塔
+鐘漏
+鐘琴
+鐘磬
+鐘形蟲
+鐘乳洞
+鐘乳石
+鐘在寺里
+擊鐘
+詩鐘
+懸鐘
+山崩鐘應
+坐鐘
+宗周鐘
+塞耳盜鐘
+二缶鐘惑
+一口鐘
+叩鐘
+盜鐘
+音聲如鐘
+應鐘
+原子鐘
+泳氣鐘
+電子鐘
+射鵰
+神鵰
+神雕像
+采石磯
+采石之戰
+采石之役
+聊齋志異
+不斷發
+部落發
+角落發
+村落發
+蛇髮女妖
+畢生發展
+對華發動
+中美發表
+尸魂界
+樹樑
+屋樑
+樑柱
+柱樑
+昇陽
+僥倖
+夏遊
+秋遊
+冬遊
+黑奴籲天錄
+林郁方
+讚歌
+編餘
+三餘
+餘墨
+唾餘
+餘韻
+歸餘
+公餘
+寬餘
+餘糧
+餘慶
+餘殃
+餘燼
+劫餘
+結餘
+燼餘
+淨餘
+餕餘
+餘暉
+餘輝
+羨餘
+餘悸
+心餘
+刑餘
+緒餘
+血餘
+朱慶餘
+諸餘
+餘論
+茶餘
+廚餘
+餘裕
+餘氣
+詩餘
+詞餘
+餘僇
+餘辜
+餘責
+餘罪
+無餘
+耳餘
+餘烈
+餘思
+鹽餘
+嬴餘
+贏餘
+王餘魚
+紆餘
+餘波
+餘杯
+餘步
+餘妙
+餘音
+餘聲
+餘明
+餘風
+餘黨
+餘毒
+餘桃
+餘桶
+餘利
+餘瀝
+餘膏
+餘光
+餘杭
+餘竅
+餘缺
+餘暇
+餘閒
+餘羨
+餘響
+餘興
+餘蓄
+餘緒
+餘珍
+餘眾
+餘酲
+餘喘
+餘食
+餘熱
+餘刃
+餘閏
+餘存
+餘業
+餘姚
+餘蔭
+餘映
+餘外
+餘威
+餘味
+餘溫
+餘勇
+多餘
+剩餘
+餘生
+有餘
+余光生
+余光中
+崑山
+崑曲
+崑腔
+崑調
+崑劇
+崑蘇
+蘇崑
diff --git a/includes/zhtable/tradphrases_exclude.manual b/includes/zhtable/tradphrases_exclude.manual
new file mode 100644
index 00000000..40dc5c09
--- /dev/null
+++ b/includes/zhtable/tradphrases_exclude.manual
@@ -0,0 +1,161 @@
+三國誌
+聊齋誌異
+北迴
+南迴
+併排
+併進
+併在
+併成
+衝衝
+臺
+著
+佈
+纔
+采
+可憐虫
+一齣
+上弔
+弔車
+弔橋
+弔嗓子
+弔床
+弔架
+弔桶
+弔桿
+弔橋
+弔燈
+弔環
+弔籃
+弔胃口
+弔臂
+弔銷
+形影相弔
+被髮
+散髮
+長髮
+髮毛
+髮端
+周而複始
+答複
+複興
+複舊
+顛複
+修複
+報複
+複活
+反複
+迴首
+彙總
+饑餓
+饑不擇食
+饑荒
+憑藉
+藉故
+藉口
+藉端
+藉詞
+藉酒
+蛋捲
+行李捲
+克裡
+纍纍
+華裡
+裡海
+瞭解
+明瞭
+發黴
+矇蔽
+矇住
+濛濛
+矇矇
+下麵
+白麵
+切麵
+和麵
+復甦
+複蘇
+甦醒
+体
+繫數
+遊擊
+馥鬱
+鬱鬱
+改製
+獃住
+獃氣
+獃子
+獃頭獃腦
+儘量
+希腊
+腊肉
+瞭如
+昇
+武鬆
+赤鬆
+黑鬆
+鬆林
+鬆科
+鬆濤
+鬆毛蟲
+鬆節油
+濕地鬆
+紮伊爾
+阿布紮比
+阿紮尼亞
+利比裡亞
+斯裡蘭卡
+烏蘇裡江
+加裡寧
+歐幾裡得
+格裡
+巴裡
+居裡
+卡裡
+墨索裡尼
+底裡
+裡人
+裡加
+裡裡
+馬裡
+裡拉
+阿裡
+裡斯
+鄰裡
+鄉裡
+百裡
+特裡
+海裡
+三元裡
+漏鬥
+春捲
+採邑
+嚮日
+佔城
+名錶
+錶面
+彆腳
+併力
+併列
+豐富多採
+採採
+尼採
+小醜
+辛醜
+整齣
+嚴複
+枯幹
+干著急
+單於
+攻剋
+剋服
+闢邪
+釐米
+後樑
+石樑
+木樑
+舊莊
+介係詞
+介繫詞
+餘年
+大阪
+阪田